diff --git a/ai/select-algorithm-typescript/.env.example b/ai/select-algorithm-typescript/.env.example new file mode 100644 index 0000000..3313ea3 --- /dev/null +++ b/ai/select-algorithm-typescript/.env.example @@ -0,0 +1,27 @@ +# ======================================== +# Azure OpenAI Embedding Settings +# ======================================== +AZURE_OPENAI_EMBEDDING_MODEL=text-embedding-3-small +AZURE_OPENAI_EMBEDDING_API_VERSION=2023-05-15 +AZURE_OPENAI_EMBEDDING_ENDPOINT=https://.openai.azure.com + +# ======================================== +# Data File Paths and Vector Configuration +# ======================================== +DATA_FILE_WITH_VECTORS=../data/Hotels_Vector.json +EMBEDDED_FIELD=DescriptionVector +EMBEDDING_DIMENSIONS=1536 +LOAD_SIZE_BATCH=100 + +# ======================================== +# MongoDB/DocumentDB Connection Settings +# ======================================== +MONGO_CLUSTER_NAME= + +# ======================================== +# Algorithm Selection (used by select-algorithm.ts) +# ======================================== +# ALGORITHM: "all" | "diskann" | "hnsw" | "ivf" +ALGORITHM=all +# SIMILARITY: "all" | "COS" | "L2" | "IP" +SIMILARITY=COS diff --git a/ai/select-algorithm-typescript/README.md b/ai/select-algorithm-typescript/README.md new file mode 100644 index 0000000..18efd9f --- /dev/null +++ b/ai/select-algorithm-typescript/README.md @@ -0,0 +1,61 @@ +# Select Algorithm - TypeScript + +Compare DiskANN, HNSW, and IVF vector index algorithms across COS, L2, and IP similarity metrics using Azure DocumentDB. + +## Prerequisites + +- Node.js 20+ +- Azure DocumentDB cluster +- Azure OpenAI resource with `text-embedding-3-small` deployment + +## Setup + +1. Copy `.env.example` to `../../.env` (repo root) and fill in your values. +2. Install dependencies: + +```bash +npm install +``` + +## Usage + +### Compare all algorithms (default: COS similarity) + +```bash +npm start +``` + +Set `ALGORITHM` and `SIMILARITY` env vars in `.env` to control which collections are queried: + +| ALGORITHM | SIMILARITY | Collections queried | +|-----------|------------|---------------------| +| `all` | `COS` | 3 (one per algorithm, COS) | +| `all` | `all` | 9 (all combinations) | +| `diskann` | `COS` | 1 (hotels_diskann_cos) | +| `diskann` | `all` | 3 (diskann Ɨ all similarities) | + +### Run single algorithm + +```bash +npm run start:diskann +npm run start:hnsw +npm run start:ivf +``` + +### Verify indexes + +```bash +npm run start:show-indexes +``` + +## Architecture + +Creates 9 collections (3 algorithms Ɨ 3 distance metrics): + +| Algorithm | COS | L2 | IP | +|-----------|-----|----|----| +| DiskANN | `hotels_diskann_cos` | `hotels_diskann_l2` | `hotels_diskann_ip` | +| HNSW | `hotels_hnsw_cos` | `hotels_hnsw_l2` | `hotels_hnsw_ip` | +| IVF | `hotels_ivf_cos` | `hotels_ivf_l2` | `hotels_ivf_ip` | + +Each collection gets its own vector index created via `db.command()` and data inserted via `insertMany()`. The main script runs `$search` aggregation queries and prints a comparison table. diff --git a/ai/select-algorithm-typescript/package-lock.json b/ai/select-algorithm-typescript/package-lock.json new file mode 100644 index 0000000..12bc62d --- /dev/null +++ b/ai/select-algorithm-typescript/package-lock.json @@ -0,0 +1,749 @@ +{ + "name": "select-algorithm-typescript", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "select-algorithm-typescript", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.11.1", + "mongodb": "^6.18.0", + "openai": "^5.16.0" + }, + "devDependencies": { + "@types/node": "^24.3.0", + "typescript": "^5.9.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.30.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.30.0.tgz", + "integrity": "sha512-HBBKfbZkMVzzF5bofvS1cXuNHFVc+gt4/HOnCmG/0hsHuZRJvJvDg/+7nTwIpoqvJc8BQp5o23rBUfisOLxR+w==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.17.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.17.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.17.0.tgz", + "integrity": "sha512-VQ5/gTLFADkwue+FohVuCqlzFPUq4xSrX8jeZe+iwZuY6moliNC8xt86qPVNYdtbQfELDf2Nu6LI+demFPHGgw==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.10", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.10.tgz", + "integrity": "sha512-0Hz7Kx4hs70KZWep/Rd7aw/qOLUF92wUOhn7ZsOuB5xNR/06NL1E2RAI9+UKH1FtvN8nD6mFjH7UKSjv6vOWvQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.17.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz", + "integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "5.23.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.23.2.tgz", + "integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/ai/select-algorithm-typescript/package.json b/ai/select-algorithm-typescript/package.json new file mode 100644 index 0000000..a65c520 --- /dev/null +++ b/ai/select-algorithm-typescript/package.json @@ -0,0 +1,27 @@ +{ + "name": "select-algorithm-typescript", + "version": "1.0.0", + "description": "Compare DiskANN, HNSW, and IVF vector index algorithms with DocumentDB", + "type": "module", + "license": "MIT", + "scripts": { + "build": "tsc", + "start": "tsc && node --env-file ../../.env dist/select-algorithm.js", + "start:diskann": "tsc && node --env-file ../../.env dist/other/diskann-only.js", + "start:hnsw": "tsc && node --env-file ../../.env dist/other/hnsw-only.js", + "start:ivf": "tsc && node --env-file ../../.env dist/other/ivf-only.js", + "start:show-indexes": "tsc && node --env-file ../../.env dist/other/show-indexes.js" + }, + "dependencies": { + "@azure/identity": "^4.11.1", + "mongodb": "^6.18.0", + "openai": "^5.16.0" + }, + "devDependencies": { + "@types/node": "^24.3.0", + "typescript": "^5.9.2" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/ai/select-algorithm-typescript/src/other/diskann-only.ts b/ai/select-algorithm-typescript/src/other/diskann-only.ts new file mode 100644 index 0000000..81c3e34 --- /dev/null +++ b/ai/select-algorithm-typescript/src/other/diskann-only.ts @@ -0,0 +1,109 @@ +import path from 'path'; +import { readFileReturnJson, getClientsPasswordless, insertData, printSearchResults } from '../utils.js'; + +// ESM specific features - create __dirname equivalent +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const config = { + query: "quintessential lodging near running trails, eateries, retail", + dbName: process.env.AZURE_DOCUMENTDB_DATABASENAME || "Hotels", + collectionName: "hotels_diskann", + indexName: "vectorIndex_diskann", + dataFile: process.env.DATA_FILE_WITH_VECTORS || '../data/Hotels_Vector.json', + batchSize: parseInt(process.env.LOAD_SIZE_BATCH || '100', 10), + embeddedField: process.env.EMBEDDED_FIELD || 'DescriptionVector', + embeddingDimensions: parseInt(process.env.EMBEDDING_DIMENSIONS || '1536', 10), + deployment: process.env.AZURE_OPENAI_EMBEDDING_MODEL!, +}; + +async function main() { + + const { aiClient, dbClient } = getClientsPasswordless(); + + try { + + if (!aiClient) { + throw new Error('AI client is not configured. Please check your environment variables.'); + } + if (!dbClient) { + throw new Error('Database client is not configured. Please check your environment variables.'); + } + + await dbClient.connect(); + const db = dbClient.db(config.dbName); + const collection = await db.createCollection(config.collectionName); + console.log('Created collection:', config.collectionName); + const data = await readFileReturnJson(path.join(__dirname, "../..", config.dataFile)); + const insertSummary = await insertData(config, collection, data); + + // Create the vector index + const indexOptions = { + createIndexes: config.collectionName, + indexes: [ + { + name: config.indexName, + key: { + [config.embeddedField]: 'cosmosSearch' + }, + cosmosSearchOptions: { + kind: 'vector-diskann', + dimensions: config.embeddingDimensions, + similarity: 'COS', + maxDegree: 32, + lBuild: 50 + } + } + ] + }; + const vectorIndexSummary = await db.command(indexOptions); + console.log('Created vector index:', config.indexName); + + // Create embedding for the query + const createEmbeddedForQueryResponse = await aiClient.embeddings.create({ + model: config.deployment, + input: [config.query] + }); + + // Perform the vector similarity search + const searchResults = await collection.aggregate([ + { + $search: { + cosmosSearch: { + vector: createEmbeddedForQueryResponse.data[0].embedding, + path: config.embeddedField, + k: 5, + lSearch: 100 + } + } + }, + { + $project: { + score: { + $meta: "searchScore" + }, + document: "$$ROOT" + } + } + ]).toArray(); + + // Print the results + printSearchResults(searchResults); + + } catch (error) { + console.error('App failed:', error); + process.exitCode = 1; + } finally { + console.log('Closing database connection...'); + if (dbClient) await dbClient.close(); + console.log('Database connection closed'); + } +} + +// Execute the main function +main().catch(error => { + console.error('Unhandled error:', error); + process.exitCode = 1; +}); diff --git a/ai/select-algorithm-typescript/src/other/hnsw-only.ts b/ai/select-algorithm-typescript/src/other/hnsw-only.ts new file mode 100644 index 0000000..ec2ec1a --- /dev/null +++ b/ai/select-algorithm-typescript/src/other/hnsw-only.ts @@ -0,0 +1,109 @@ +import path from 'path'; +import { readFileReturnJson, getClientsPasswordless, insertData, printSearchResults } from '../utils.js'; + +// ESM specific features - create __dirname equivalent +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const config = { + query: "quintessential lodging near running trails, eateries, retail", + dbName: process.env.AZURE_DOCUMENTDB_DATABASENAME || "Hotels", + collectionName: "hotels_hnsw", + indexName: "vectorIndex_hnsw", + dataFile: process.env.DATA_FILE_WITH_VECTORS || '../data/Hotels_Vector.json', + batchSize: parseInt(process.env.LOAD_SIZE_BATCH || '100', 10), + embeddedField: process.env.EMBEDDED_FIELD || 'DescriptionVector', + embeddingDimensions: parseInt(process.env.EMBEDDING_DIMENSIONS || '1536', 10), + deployment: process.env.AZURE_OPENAI_EMBEDDING_MODEL!, +}; + +async function main() { + + const { aiClient, dbClient } = getClientsPasswordless(); + + try { + + if (!aiClient) { + throw new Error('AI client is not configured. Please check your environment variables.'); + } + if (!dbClient) { + throw new Error('Database client is not configured. Please check your environment variables.'); + } + + await dbClient.connect(); + const db = dbClient.db(config.dbName); + const collection = await db.createCollection(config.collectionName); + console.log('Created collection:', config.collectionName); + const data = await readFileReturnJson(path.join(__dirname, "../..", config.dataFile)); + const insertSummary = await insertData(config, collection, data); + + // Create the vector index + const indexOptions = { + createIndexes: config.collectionName, + indexes: [ + { + name: config.indexName, + key: { + [config.embeddedField]: 'cosmosSearch' + }, + cosmosSearchOptions: { + kind: 'vector-hnsw', + m: 16, + efConstruction: 64, + similarity: 'COS', + dimensions: config.embeddingDimensions + } + } + ] + }; + const vectorIndexSummary = await db.command(indexOptions); + console.log('Created vector index:', config.indexName); + + // Create embedding for the query + const createEmbeddedForQueryResponse = await aiClient.embeddings.create({ + model: config.deployment, + input: [config.query] + }); + + // Perform the vector similarity search + const searchResults = await collection.aggregate([ + { + $search: { + cosmosSearch: { + vector: createEmbeddedForQueryResponse.data[0].embedding, + path: config.embeddedField, + k: 5, + efSearch: 80 + } + } + }, + { + $project: { + score: { + $meta: "searchScore" + }, + document: "$$ROOT" + } + } + ]).toArray(); + + // Print the results + printSearchResults(searchResults); + + } catch (error) { + console.error('App failed:', error); + process.exitCode = 1; + } finally { + console.log('Closing database connection...'); + if (dbClient) await dbClient.close(); + console.log('Database connection closed'); + } +} + +// Execute the main function +main().catch(error => { + console.error('Unhandled error:', error); + process.exitCode = 1; +}); diff --git a/ai/select-algorithm-typescript/src/other/ivf-only.ts b/ai/select-algorithm-typescript/src/other/ivf-only.ts new file mode 100644 index 0000000..84bba5d --- /dev/null +++ b/ai/select-algorithm-typescript/src/other/ivf-only.ts @@ -0,0 +1,109 @@ +import path from 'path'; +import { readFileReturnJson, getClientsPasswordless, insertData, printSearchResults } from '../utils.js'; + +// ESM specific features - create __dirname equivalent +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const config = { + query: "quintessential lodging near running trails, eateries, retail", + dbName: process.env.AZURE_DOCUMENTDB_DATABASENAME || "Hotels", + collectionName: "hotels_ivf", + indexName: "vectorIndex_ivf", + dataFile: process.env.DATA_FILE_WITH_VECTORS || '../data/Hotels_Vector.json', + batchSize: parseInt(process.env.LOAD_SIZE_BATCH || '100', 10), + embeddedField: process.env.EMBEDDED_FIELD || 'DescriptionVector', + embeddingDimensions: parseInt(process.env.EMBEDDING_DIMENSIONS || '1536', 10), + deployment: process.env.AZURE_OPENAI_EMBEDDING_MODEL!, +}; + +async function main() { + + const { aiClient, dbClient } = getClientsPasswordless(); + + try { + + if (!aiClient) { + throw new Error('AI client is not configured. Please check your environment variables.'); + } + if (!dbClient) { + throw new Error('Database client is not configured. Please check your environment variables.'); + } + + await dbClient.connect(); + const db = dbClient.db(config.dbName); + const collection = await db.createCollection(config.collectionName); + console.log('Created collection:', config.collectionName); + const data = await readFileReturnJson(path.join(__dirname, "../..", config.dataFile)); + const insertSummary = await insertData(config, collection, data); + + // Create the vector index + const indexOptions = { + createIndexes: config.collectionName, + indexes: [ + { + name: config.indexName, + key: { + [config.embeddedField]: 'cosmosSearch' + }, + cosmosSearchOptions: { + kind: 'vector-ivf', + numLists: 1, + similarity: 'COS', + dimensions: config.embeddingDimensions + } + } + ] + }; + const vectorIndexSummary = await db.command(indexOptions); + console.log('Created vector index:', config.indexName); + + // Create embedding for the query + const createEmbeddedForQueryResponse = await aiClient.embeddings.create({ + model: config.deployment, + input: [config.query] + }); + + // Perform the vector similarity search + const searchResults = await collection.aggregate([ + { + $search: { + cosmosSearch: { + vector: createEmbeddedForQueryResponse.data[0].embedding, + path: config.embeddedField, + k: 5, + nProbes: 1 + }, + returnStoredSource: true + } + }, + { + $project: { + score: { + $meta: "searchScore" + }, + document: "$$ROOT" + } + } + ]).toArray(); + + // Print the results + printSearchResults(searchResults); + + } catch (error) { + console.error('App failed:', error); + process.exitCode = 1; + } finally { + console.log('Closing database connection...'); + if (dbClient) await dbClient.close(); + console.log('Database connection closed'); + } +} + +// Execute the main function +main().catch(error => { + console.error('Unhandled error:', error); + process.exitCode = 1; +}); diff --git a/ai/select-algorithm-typescript/src/other/show-indexes.ts b/ai/select-algorithm-typescript/src/other/show-indexes.ts new file mode 100644 index 0000000..39dd19d --- /dev/null +++ b/ai/select-algorithm-typescript/src/other/show-indexes.ts @@ -0,0 +1,81 @@ +import { getClientsPasswordless } from '../utils.js'; + +async function getAllDatabases(dbClient): Promise { + try { + const dbList = await dbClient.db().admin().listDatabases({ nameOnly: true }); + return dbList.databases + .map((db: any) => db.name) + .filter((name: string) => !['admin', 'config', 'local'].includes(name)); + } catch (error) { + console.error('Error listing databases:', error); + return []; + } +} + +async function getAllCollections(db): Promise { + try { + const collections = await db.listCollections().toArray(); + return collections.map((coll: any) => coll.name); + } catch (error) { + console.error(`Error listing collections for database ${db.databaseName}:`, error); + return []; + } +} + +async function getAllIndexes(db, collectionName: string): Promise { + try { + const collection = db.collection(collectionName); + const indexes = await collection.indexes(); + console.log(`\n šŸ—ƒļø COLLECTION: ${collectionName} (${indexes.length} indexes)`); + console.log(JSON.stringify(indexes, null, 2)); + } catch (error) { + console.error(`Error listing indexes for collection ${collectionName}:`, error); + } +} + +async function main() { + + const { dbClient } = getClientsPasswordless(); + + if (!dbClient) { + throw new Error('Database client is not configured. Please check your environment variables.'); + } + + try { + await dbClient.connect(); + const dbNames = await getAllDatabases(dbClient); + + if (dbNames.length === 0) { + console.log('No databases found or access denied'); + return; + } + + for (const dbName of dbNames) { + const db = dbClient.db(dbName); + const collections = await getAllCollections(db); + + if (collections.length === 0) { + console.log(`Database '${dbName}': No collections found`); + continue; + } + + console.log(`\nšŸ“‚ DATABASE: ${dbName} (${collections.length} collections)`); + + for (const collName of collections) { + await getAllIndexes(db, collName); + } + } + } catch (error) { + console.error('Index retrieval failed:', error); + process.exitCode = 1; + } finally { + console.log('\nClosing database connection...'); + if (dbClient) await dbClient.close(); + console.log('Database connection closed'); + } +} + +main().catch(error => { + console.error('Unhandled error:', error); + process.exitCode = 1; +}); diff --git a/ai/select-algorithm-typescript/src/select-algorithm.ts b/ai/select-algorithm-typescript/src/select-algorithm.ts new file mode 100644 index 0000000..eb88388 --- /dev/null +++ b/ai/select-algorithm-typescript/src/select-algorithm.ts @@ -0,0 +1,287 @@ +import path from 'path'; +import { readFileReturnJson, getClientsPasswordless, insertData, printComparisonTable } from './utils.js'; + +// ESM specific features - create __dirname equivalent +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Validate required environment variables at startup +const requiredEnvVars = [ + 'MONGO_CLUSTER_NAME', + 'AZURE_OPENAI_EMBEDDING_ENDPOINT', + 'AZURE_OPENAI_EMBEDDING_MODEL', + 'DATA_FILE_WITH_VECTORS' +]; + +const missing = requiredEnvVars.filter(v => !process.env[v]); +if (missing.length > 0) { + console.error(`Missing required environment variables: ${missing.join(', ')}`); + console.error('See .env.example for required values.'); + process.exit(1); +} + +type Algorithm = 'diskann' | 'hnsw' | 'ivf'; +type Similarity = 'COS' | 'L2' | 'IP'; + +const ALGORITHMS: Algorithm[] = ['diskann', 'hnsw', 'ivf']; +const SIMILARITIES: Similarity[] = ['COS', 'L2', 'IP']; + +const ALGORITHM_LABELS: Record = { + diskann: 'DiskANN', + hnsw: 'HNSW', + ivf: 'IVF', +}; + +// Index creation configs per algorithm +function getIndexOptions( + collectionName: string, + indexName: string, + embeddedField: string, + dimensions: number, + algorithm: Algorithm, + similarity: Similarity +) { + const base = { + createIndexes: collectionName, + indexes: [ + { + name: indexName, + key: { [embeddedField]: 'cosmosSearch' }, + cosmosSearchOptions: {} as Record, + }, + ], + }; + + switch (algorithm) { + case 'diskann': + base.indexes[0].cosmosSearchOptions = { + kind: 'vector-diskann', + dimensions, + similarity, + maxDegree: 32, + lBuild: 50, + }; + break; + case 'hnsw': + base.indexes[0].cosmosSearchOptions = { + kind: 'vector-hnsw', + dimensions, + similarity, + m: 16, + efConstruction: 64, + }; + break; + case 'ivf': + base.indexes[0].cosmosSearchOptions = { + kind: 'vector-ivf', + dimensions, + similarity, + numLists: 1, + }; + break; + } + + return base; +} + +// Algorithm-specific query params +function getSearchPipeline( + queryEmbedding: number[], + embeddedField: string, + k: number, + algorithm: Algorithm +) { + const cosmosSearch: Record = { + vector: queryEmbedding, + path: embeddedField, + k, + }; + + // Add algorithm-specific search params + switch (algorithm) { + case 'diskann': + cosmosSearch.lSearch = 100; + break; + case 'hnsw': + cosmosSearch.efSearch = 80; + break; + case 'ivf': + cosmosSearch.nProbes = 1; + break; + } + + return [ + { $search: { cosmosSearch } }, + { $project: { score: { $meta: "searchScore" }, document: "$$ROOT" } }, + ]; +} + +/** + * Determine which collections to create/query based on ALGORITHM and SIMILARITY env vars. + * Collection naming: hotels_{algorithm}_{similarity} + */ +function getTargetCollections( + algorithmEnv: string, + similarityEnv: string +): Array<{ collectionName: string; algorithm: Algorithm; similarity: Similarity }> { + const algorithms: Algorithm[] = + algorithmEnv === 'all' ? ALGORITHMS : [algorithmEnv as Algorithm]; + const similarities: Similarity[] = + similarityEnv === 'all' ? SIMILARITIES : [similarityEnv as Similarity]; + + const targets: Array<{ collectionName: string; algorithm: Algorithm; similarity: Similarity }> = []; + + for (const alg of algorithms) { + if (!ALGORITHMS.includes(alg)) { + throw new Error(`Invalid ALGORITHM '${alg}'. Must be one of: all, ${ALGORITHMS.join(', ')}`); + } + for (const sim of similarities) { + if (!SIMILARITIES.includes(sim)) { + throw new Error(`Invalid SIMILARITY '${sim}'. Must be one of: all, ${SIMILARITIES.join(', ')}`); + } + targets.push({ + collectionName: `hotels_${alg}_${sim.toLowerCase()}`, + algorithm: alg, + similarity: sim, + }); + } + } + + return targets; +} + +async function main() { + const { aiClient, dbClient } = getClientsPasswordless(); + + try { + if (!aiClient) { + throw new Error('Azure OpenAI client is not configured. Please check your environment variables.'); + } + if (!dbClient) { + throw new Error('Database client is not configured. Please check your environment variables.'); + } + + const dbName = process.env.AZURE_DOCUMENTDB_DATABASENAME || 'Hotels'; + const embeddedField = process.env.EMBEDDED_FIELD || 'DescriptionVector'; + const embeddingDimensions = parseInt(process.env.EMBEDDING_DIMENSIONS || '1536', 10); + const dataFile = process.env.DATA_FILE_WITH_VECTORS || '../data/Hotels_Vector.json'; + const deployment = process.env.AZURE_OPENAI_EMBEDDING_MODEL!; + const batchSize = parseInt(process.env.LOAD_SIZE_BATCH || '100', 10); + const algorithmEnv = (process.env.ALGORITHM || 'all').trim().toLowerCase(); + const similarityEnv = (process.env.SIMILARITY || 'COS').trim().toUpperCase(); + const searchQuery = 'quintessential lodging near running trails, eateries, retail'; + + const targets = getTargetCollections(algorithmEnv, similarityEnv); + + console.log(`\nšŸ”¬ Vector Algorithm Comparison`); + console.log(` Database: ${dbName}`); + console.log(` Algorithms: ${algorithmEnv}`); + console.log(` Similarity: ${similarityEnv}`); + console.log(` Collections to query: ${targets.map(t => t.collectionName).join(', ')}`); + console.log(` Search query: "${searchQuery}"\n`); + + await dbClient.connect(); + const db = dbClient.db(dbName); + + // Load data once (shared across collections) + const data = await readFileReturnJson(path.join(__dirname, '..', dataFile)); + + // Generate query embedding once (reuse across collections) + console.log('Generating query embedding...'); + const embeddingResponse = await aiClient.embeddings.create({ + model: deployment, + input: [searchQuery], + }); + const queryEmbedding = embeddingResponse.data[0].embedding; + if (queryEmbedding.length !== embeddingDimensions) { + throw new Error( + `Embedding dimension mismatch: expected ${embeddingDimensions}, got ${queryEmbedding.length}. ` + + `Verify AZURE_OPENAI_EMBEDDING_MODEL matches the configured EMBEDDING_DIMENSIONS.` + ); + } + console.log(`Query embedding: ${queryEmbedding.length} dimensions\n`); + + const config = { batchSize }; + + const comparisonResults: Array<{ + collectionName: string; + algorithm: string; + similarity: string; + searchResults: any[]; + latencyMs: number; + }> = []; + + for (const target of targets) { + console.log(`\n━━━ ${ALGORITHM_LABELS[target.algorithm]} / ${target.similarity} ━━━`); + console.log(`Collection: ${target.collectionName}`); + + try { + // Create collection (drops existing to ensure clean state) + try { + await db.dropCollection(target.collectionName); + } catch { + // Collection may not exist yet + } + const collection = await db.createCollection(target.collectionName); + console.log('Created collection:', target.collectionName); + + // Insert data + const insertSummary = await insertData(config, collection, data); + console.log(`Inserted: ${insertSummary.inserted}/${insertSummary.total}`); + + // Create vector index + const indexName = `vectorIndex_${target.algorithm}_${target.similarity.toLowerCase()}`; + const indexOptions = getIndexOptions( + target.collectionName, + indexName, + embeddedField, + embeddingDimensions, + target.algorithm, + target.similarity + ); + await db.command(indexOptions); + console.log('Created vector index:', indexName); + + // Run vector search + console.log('Executing vector search...'); + const startTime = Date.now(); + + const pipeline = getSearchPipeline(queryEmbedding, embeddedField, 5, target.algorithm); + const searchResults = await collection.aggregate(pipeline).toArray(); + + const latencyMs = Date.now() - startTime; + + comparisonResults.push({ + collectionName: target.collectionName, + algorithm: ALGORITHM_LABELS[target.algorithm], + similarity: target.similarity, + searchResults, + latencyMs, + }); + + console.log(`āœ“ ${searchResults.length} results, ${latencyMs}ms`); + } catch (error) { + console.error(`āœ— Error with ${target.collectionName}:`, (error as Error).message); + } + } + + // Print comparison table + if (comparisonResults.length > 0) { + printComparisonTable(comparisonResults); + } + } catch (error) { + console.error('App failed:', error); + process.exitCode = 1; + } finally { + console.log('\nClosing database connection...'); + if (dbClient) await dbClient.close(); + console.log('Database connection closed'); + } +} + +main().catch(error => { + console.error('Unhandled error:', error); + process.exitCode = 1; +}); diff --git a/ai/select-algorithm-typescript/src/utils.ts b/ai/select-algorithm-typescript/src/utils.ts new file mode 100644 index 0000000..aa8e462 --- /dev/null +++ b/ai/select-algorithm-typescript/src/utils.ts @@ -0,0 +1,190 @@ +import { Collection, Document, MongoClient, OIDCResponse, OIDCCallbackParams } from 'mongodb'; +import { AzureOpenAI } from 'openai/index.js'; +import { promises as fs } from "fs"; +import { AccessToken, DefaultAzureCredential, TokenCredential, getBearerTokenProvider } from '@azure/identity'; + +// Define a type for JSON data +export type JsonData = Record; + +export const AzureIdentityTokenCallback = async (params: OIDCCallbackParams, credential: TokenCredential): Promise => { + const tokenResponse: AccessToken | null = await credential.getToken(['https://ossrdbms-aad.database.windows.net/.default']); + return { + accessToken: tokenResponse?.token || '', + expiresInSeconds: (tokenResponse?.expiresOnTimestamp || 0) - Math.floor(Date.now() / 1000) + }; +}; + +export function getClientsPasswordless(): { aiClient: AzureOpenAI | null; dbClient: MongoClient | null } { + let aiClient: AzureOpenAI | null = null; + let dbClient: MongoClient | null = null; + + // Validate all required environment variables upfront + const endpoint = process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT!; + const deployment = process.env.AZURE_OPENAI_EMBEDDING_MODEL!; + const clusterName = process.env.MONGO_CLUSTER_NAME!; + + if (!endpoint || !deployment || !clusterName) { + throw new Error('Missing required environment variables: AZURE_OPENAI_EMBEDDING_ENDPOINT, AZURE_OPENAI_EMBEDDING_MODEL, MONGO_CLUSTER_NAME'); + } + + console.log(`Using Azure OpenAI Embedding Deployment/Model: ${deployment}`); + + const credential = new DefaultAzureCredential(); + + // For Azure OpenAI with DefaultAzureCredential + { + const scope = "https://cognitiveservices.azure.com/.default"; + const azureADTokenProvider = getBearerTokenProvider(credential, scope); + aiClient = new AzureOpenAI({ + apiVersion: "2024-10-21", + endpoint, + deployment, + azureADTokenProvider, + timeout: 30000, + maxRetries: 3, + }); + } + + // For DocumentDB with DefaultAzureCredential (uses signed-in user) + { + dbClient = new MongoClient( + `mongodb+srv://${clusterName}.mongocluster.cosmos.azure.com/`, { + connectTimeoutMS: 120000, + tls: true, + retryWrites: false, + maxIdleTimeMS: 120000, + authMechanism: 'MONGODB-OIDC', + authMechanismProperties: { + OIDC_CALLBACK: (params: OIDCCallbackParams) => AzureIdentityTokenCallback(params, credential), + ALLOWED_HOSTS: ['*.azure.com'] + } + } + ); + } + + return { aiClient, dbClient }; +} + +export async function readFileReturnJson(filePath: string): Promise { + + console.log(`Reading JSON file from ${filePath}`); + + const fileAsString = await fs.readFile(filePath, "utf-8"); + return JSON.parse(fileAsString); +} + +export async function insertData(config: { batchSize: number }, collection: Collection, data: Document[]) { + console.log(`Processing in batches of ${config.batchSize}...`); + const totalBatches = Math.ceil(data.length / config.batchSize); + + let inserted = 0; + let failed = 0; + + for (let i = 0; i < totalBatches; i++) { + const start = i * config.batchSize; + const end = Math.min(start + config.batchSize, data.length); + const batch = data.slice(start, end); + + try { + const result = await collection.insertMany(batch, { ordered: false }); + inserted += result.insertedCount || 0; + console.log(`Batch ${i + 1} complete: ${result.insertedCount} inserted`); + } catch (error: any) { + if (error?.writeErrors) { + console.error(`Error in batch ${i + 1}: ${error?.writeErrors.length} failures`); + failed += error?.writeErrors.length; + inserted += batch.length - error?.writeErrors.length; + } else { + console.error(`Error in batch ${i + 1}:`, error); + failed += batch.length; + } + } + + // Small pause between batches to reduce resource contention + if (i < totalBatches - 1) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + // Create standard field indexes + const indexColumns = ["HotelId", "Category", "Description", "Description_fr"]; + for (const col of indexColumns) { + const indexSpec: Record = {}; + indexSpec[col] = 1; + await collection.createIndex(indexSpec); + } + + return { total: data.length, inserted, failed }; +} + +export function printSearchResults(searchResults: Document[]) { + if (!searchResults || searchResults.length === 0) { + console.log('No search results found.'); + return; + } + + searchResults.map((result: Document, index: number) => { + const { document, score } = result; + console.log(`${index + 1}. HotelName: ${document.HotelName}, Score: ${score.toFixed(4)}`); + }); +} + +/** + * Print a side-by-side comparison table of vector search results across collections + */ +export function printComparisonTable( + results: Array<{ + collectionName: string; + algorithm: string; + similarity: string; + searchResults: any[]; + latencyMs: number; + }> +): void { + console.log('\n╔══════════════════════════════════════════════════════════════════════════════════╗'); + console.log('ā•‘ Vector Algorithm Comparison Results ā•‘'); + console.log('╠══════════════════════════════════════════════════════════════════════════════════╣'); + + // Header + console.log( + 'ā•‘ ' + + 'Algorithm'.padEnd(12) + + 'Similarity'.padEnd(14) + + 'Top Result'.padEnd(24) + + 'Score'.padEnd(12) + + 'Latency(ms)'.padEnd(14) + + 'ā•‘' + ); + console.log('╠══════════════════════════════════════════════════════════════════════════════════╣'); + + for (const r of results) { + const topResult = r.searchResults[0]; + const topName = topResult ? (topResult.document.HotelName as string).substring(0, 22) : 'N/A'; + const topScore = topResult ? topResult.score.toFixed(4) : 'N/A'; + + console.log( + 'ā•‘ ' + + r.algorithm.padEnd(12) + + r.similarity.padEnd(14) + + topName.padEnd(24) + + topScore.padEnd(12) + + r.latencyMs.toFixed(0).padEnd(14) + + 'ā•‘' + ); + } + + console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•'); + + // Detailed results per collection + for (const r of results) { + console.log(`\n--- ${r.algorithm} / ${r.similarity} (${r.collectionName}) ---`); + if (r.searchResults.length === 0) { + console.log(' No results.'); + continue; + } + r.searchResults.forEach((item: Document, i: number) => { + console.log(` ${i + 1}. ${item.document.HotelName}, Score: ${item.score.toFixed(4)}`); + }); + console.log(` Latency: ${r.latencyMs.toFixed(0)}ms`); + } +} diff --git a/ai/select-algorithm-typescript/tsconfig.json b/ai/select-algorithm-typescript/tsconfig.json new file mode 100644 index 0000000..08e23d0 --- /dev/null +++ b/ai/select-algorithm-typescript/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "nodenext", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +}