{"_id":"@anokye-labs/kbexplorer-search","_rev":"2-aaf9663a36f6c3cb970daf9af9a85bac","name":"@anokye-labs/kbexplorer-search","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@anokye-labs/kbexplorer-search","version":"0.1.0","keywords":["kbexplorer","semantic-search","knowledge-graph","embeddings","vector-search"],"author":{"name":"Anokye Labs"},"license":"MIT","_id":"@anokye-labs/kbexplorer-search@0.1.0","maintainers":[{"name":"hoopsomuah","email":"hoop@somuah.com"}],"homepage":"https://github.com/anokye-labs/kbexplorer-search#readme","bugs":{"url":"https://github.com/anokye-labs/kbexplorer-search/issues"},"bin":{"kbexplorer-search":"bin/kbexplorer-search.js"},"dist":{"shasum":"0064e0465c80e1305bb77c3ab094906ad70c5d87","tarball":"https://registry.npmjs.org/@anokye-labs/kbexplorer-search/-/kbexplorer-search-0.1.0.tgz","fileCount":80,"integrity":"sha512-X4aMKdh3RsjKtTRmNJJnoXSJRq/4jxsDd268CLxEs0zsLf6bLBSaXXAExaA8ZoY1DVDhU23PZrQ/7kAlr4MwsA==","signatures":[{"sig":"MEUCIQDDYNx4kTsOl/x5NAHU9RHs8MYuJg4EEl3ubem/pBzUvAIgU+Zmf9o7mKqC4ZQUVrlMM6UoBX9yQ/spIBYGc75QNBQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":208326},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"f74b3d1a191eb531945f01c354b397d4f30f6d3f","scripts":{"lint":"eslint src/ tests/","test":"vitest run","build":"tsc -b","serve":"node bin/kbexplorer-search.js serve","prepare":"npm run build","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"hoopsomuah","email":"hoop@somuah.com"},"repository":{"url":"git+https://github.com/anokye-labs/kbexplorer-search.git","type":"git"},"_npmVersion":"11.16.0","description":"Semantic search companion module for kbexplorer — index production and consumption over knowledge graphs","directories":{},"_nodeVersion":"26.3.0","dependencies":{"@anokye-labs/kbexplorer-core":"github:anokye-labs/kbexplorer-core#v0.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.27.0","vitest":"^3.2.1","typescript":"^5.8.3","@types/node":"^22.15.0","typescript-eslint":"^8.62.0"},"optionalDependencies":{"faiss-node":"^0.5.1"},"_npmOperationalInternal":{"tmp":"tmp/kbexplorer-search_0.1.0_1783048151490_0.30243490259427674","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"_id":"@anokye-labs/kbexplorer-search@0.1.1","bin":{"kbexplorer-search":"bin/kbexplorer-search.js"},"bugs":{"url":"https://github.com/anokye-labs/kbexplorer-search/issues"},"dist":{"shasum":"f4afc789d0c4fc3a9f511cfb425474d639d79f4d","tarball":"https://registry.npmjs.org/@anokye-labs/kbexplorer-search/-/kbexplorer-search-0.1.1.tgz","fileCount":80,"integrity":"sha512-0B92i3OMDSV8LABRlm191TxIIZeR55VU7IqvrjszKnZz7JEyhdEfbIG/GOoBU1Ts8FFyttV+k1bx8Rt53oxXCA==","signatures":[{"sig":"MEYCIQC3g2u+rzAUHiL6h41fag7UYge0CNGFDdMTceoubuuA8QIhAMVmHnYIbWHkC/BCWQWv2R56sdR2g2aMqvvWOjCV6LZS","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIB09ziIrfPyv/tfPugirRwB0ONnDWiWF1WtaMQomoLd1AiBhBlJ9SyWprLnj7EMzEMO46ianY5fd68iz7/6JL9tKZg=="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@anokye-labs%2fkbexplorer-search@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":206574},"main":"./dist/index.js","name":"@anokye-labs/kbexplorer-search","type":"module","types":"./dist/index.d.ts","author":{"name":"Anokye Labs"},"engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"25e93972f618243067fc64a912040f61313e0d83","license":"MIT","scripts":{"lint":"eslint src/ tests/","test":"vitest run","build":"tsc -b","serve":"node bin/kbexplorer-search.js serve","prepare":"npm run build","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"version":"0.1.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:31ba2e6c-1ee6-4bd5-8154-ebd13f75816a"}},"homepage":"https://github.com/anokye-labs/kbexplorer-search#readme","keywords":["kbexplorer","semantic-search","knowledge-graph","embeddings","vector-search"],"repository":{"url":"git+https://github.com/anokye-labs/kbexplorer-search.git","type":"git"},"_npmVersion":"12.0.2","description":"Semantic search companion module for kbexplorer — index production and consumption over knowledge graphs","directories":{},"maintainers":[{"name":"hoopsomuah","email":"hoop@somuah.com"}],"_nodeVersion":"22.23.2","dependencies":{"@anokye-labs/kbexplorer-core":"^0.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.27.0","vitest":"^3.2.1","typescript":"^5.8.3","@types/node":"^22.15.0","typescript-eslint":"^8.62.0"},"optionalDependencies":{"faiss-node":"^0.5.1"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/kbexplorer-search_0.1.1_1787446560416_0.9403241561957385"}}},"time":{"created":"2026-07-03T03:09:11.343Z","modified":"2026-08-23T00:56:00.951Z","0.1.0":"2026-07-03T03:09:11.633Z","0.1.1":"2026-08-23T00:56:00.525Z"},"bugs":{"url":"https://github.com/anokye-labs/kbexplorer-search/issues"},"author":{"name":"Anokye Labs"},"license":"MIT","homepage":"https://github.com/anokye-labs/kbexplorer-search#readme","keywords":["kbexplorer","semantic-search","knowledge-graph","embeddings","vector-search"],"repository":{"url":"git+https://github.com/anokye-labs/kbexplorer-search.git","type":"git"},"description":"Semantic search companion module for kbexplorer — index production and consumption over knowledge graphs","maintainers":[{"name":"hoopsomuah","email":"hoop@somuah.com"}],"readme":"# @anokye-labs/kbexplorer-search\n\nSemantic search companion module for [kbexplorer](https://github.com/anokye-labs/kbexplorer-template) — derive, validate, and serve semantic search over knowledge graphs.\n\n## Roles\n\n### Index Production\n\nReads the kbexplorer content model, derives searchable units from the graph, generates embeddings, and writes checked-in search artifacts. Runs locally, in CI, or in GitHub Actions whenever the knowledge base changes.\n\nDriven through the [`kbx` CLI](https://github.com/anokye-labs/kbexplorer-cli), which builds the graph from `content/` and delegates to this module:\n\n```bash\nkbx search-index          # extract + embed + write artifacts to .search/\nkbx search-index --check  # CI drift gate (no API calls)\n```\n\n### Index Consumption\n\nLoads checked-in artifacts, builds an efficient vector index, embeds incoming queries, and returns kbexplorer-native results: node IDs, titles, paths, clusters, snippets, scores, and graph-aware context.\n\n```bash\nkbx search \"how does audit validation work?\"\n```\n\n## Search providers\n\n| Provider | Credentials | Index artifacts | Query scoring |\n|----------|-------------|------------------|----------------|\n| `openai` (default) | `OPENAI_API_KEY` | `units.json` + `vectors.json` + `index-meta.json` | Cosine similarity |\n| `lexical` | none — no network, no API key | `units.json` + `vectors.json` (empty) + `index-meta.json` (`providerType: \"lexical\"`) + `lexical-index.json` | Okapi BM25 |\n\nThe `lexical` provider (`src/providers/lexical.ts`) is a deterministic, zero-credential BM25 term index — it backs the `kbx` onboarding \"local\" search mode, which has no API key available. Build and query it directly from the library API:\n\n```ts\nimport { extractSearchUnits, computeContentHash, buildLexicalIndex, createLexicalSearchEngine, writeLexicalArtifacts } from '@anokye-labs/kbexplorer-search';\n\nconst units = extractSearchUnits(graph);\nconst index = buildLexicalIndex(units);        // deterministic BM25 term index\nconst contentHash = computeContentHash(graph); // SHA-256 of the canonical graph, same input the drift gate checks\nwriteLexicalArtifacts('.search', units, index, contentHash); // same checked-in shape as embedding builds\n\nconst engine = createLexicalSearchEngine(units, index);\nconst results = await engine.search('how does audit validation work?'); // same SearchResult shape as the cosine engine\n```\n\n`lexical-index.json` is additive to the standard artifact set, so `readArtifacts`/`checkDrift` (the `--check` drift gate) work unchanged against a lexical index directory. `LexicalProvider` is also registered in the provider registry (`getProvider('lexical', ...)` / `listProviders()`) for discovery; its `embed()` intentionally throws, since BM25 needs corpus-wide statistics that a stateless per-call embedding cannot carry — the real query path is `createLexicalSearchEngine`.\n\n## Accelerated search (optional)\n\n`createFaissEngine` builds a [FAISS](https://github.com/facebookresearch/faiss) `IndexFlatIP` from the checked-in vectors for faster k-NN on large indexes. FAISS is **runtime acceleration only** — the portable JSON artifacts remain the durable source of truth (see `AGENTS.md`), so nothing depends on it being present.\n\n`faiss-node` is declared as an `optionalDependency`, not a hard dependency — it ships prebuilt native binaries for a subset of platforms/Node versions, and `npm install` will skip it silently if none matches (or if no native build toolchain is available), same as any other optional dependency. To opt in:\n\n```bash\nnpm install faiss-node\n```\n\nWhen `faiss-node` isn't installed (or fails to load for any reason), `createFaissEngine` logs a clear message and transparently falls back to the pure-JS cosine engine (`search-engine.ts`) — same `SearchEngine` interface, same `SearchResult` shape, just without the native acceleration:\n\n```\nkbexplorer-search: FAISS-accelerated search unavailable (faiss-node is not installed or has\nno prebuilt binary for this platform) — using the pure-JS cosine engine instead. See the\nREADME for optional install instructions if you want accelerated k-NN on large indexes.\n```\n\nPass `{ fallback: false }` to `createFaissEngine` to throw instead of falling back (e.g. if a deployment wants to fail fast when acceleration is expected but missing).\n\n## Running the search service\n\nThe browser SPA ([kbexplorer-template](https://github.com/anokye-labs/kbexplorer-template)) consumes search over HTTP. Start the localhost service straight from this package — no extra wiring needed:\n\n```bash\n# from a repo that has checked-in .search/ artifacts\nnpx @anokye-labs/kbexplorer-search serve --dir .search --port 7700\n# requires OPENAI_API_KEY (or another configured provider) to embed queries\n```\n\nThen point the template at it:\n\n```bash\nVITE_SEARCH_SERVICE_URL=http://127.0.0.1:7700 npm run dev   # in kbexplorer-template\n```\n\n### HTTP contract\n\n| Method & path | Body | Response |\n|---------------|------|----------|\n| `GET /health` | — | `{ status, unitCount, model }` |\n| `GET /stats`  | — | `{ unitCount, model, dimensions, contentHash, version }` |\n| `POST /search`| `{ query, limit?, cluster?, entityType?, minScore?, graphRanking? }` | `{ results: SearchResult[], suggestions: RelatedSuggestion[] }` |\n\nWhen `graphRanking: true`, results are re-ranked with graph structure and `suggestions` (related graph neighbors not already in the result set) are returned; otherwise `suggestions` is `[]`.\n\n## Install\n\n```bash\nnpm install @anokye-labs/kbexplorer-search\n```\n\nNot yet published to npm? Install straight from GitHub until the first release lands: `npm install github:anokye-labs/kbexplorer-search`.\n\nYou normally don't install this directly — the `kbx` CLI depends on it for index production and queries, and the template talks to the `serve` service over HTTP. Install it directly only when embedding the library API (`createSearchEngine`, `createSearchServer`, `extractSearchUnits`, …) in your own tooling.\n\n## How it fits the kbx system\n\n- [**kbexplorer-core**](https://github.com/anokye-labs/kbexplorer-core) — the shared graph contracts (`KBNode` / `KBEdge` / `KBGraph`). This module consumes them to derive `SearchUnit`s.\n- [**kbexplorer-cli** (`kbx`)](https://github.com/anokye-labs/kbexplorer-cli) — builds the graph from local content and drives `search-index` / `search`.\n- [**kbexplorer-template**](https://github.com/anokye-labs/kbexplorer-template) — the SPA; calls `POST /search` on the `serve` service via `VITE_SEARCH_SERVICE_URL`.\n\n## Contract\n\n- **kbexplorer** defines and renders the knowledge graph.\n- **kbexplorer-search** derives, validates, and serves semantic search over that graph.\n\nSearch artifacts are deterministic, reviewable build outputs tied to the exact version of the kbexplorer graph. The repository owns the semantic search corpus; the service only provides query execution.\n\n## Access labels\n\nThe index-build path respects access labels carried on nodes/edges\n(`KBAccessLabel { classification, visibility, labels[] }`). kbx **labels**; the\nhost **enforces** — search performs **no** principal evaluation.\n\n- **Default-SAFE (`exclude`):** nodes whose `classification` is `confidential`,\n  `restricted`, or `unknown`, or whose `visibility` is `private`, produce **no**\n  `SearchUnit` and **no** vector. They never reach `units.json`/`vectors.json`,\n  so even titles cannot leak via search — including indirectly, through a\n  *neighboring public unit's* embedded text, `connections[]`, `parentId`, or\n  `metadata.{neighborTitles,hierarchyPath}`. `extractSearchUnits` derives all\n  of that adjacency/context data from a node map and edge list that are\n  filtered *before* any connections, neighbor titles, or hierarchy paths are\n  built, so an excluded node's title/id is unreachable from any surviving\n  unit. See `tests/extract.test.ts`'s \"access-exclusion leak regression\"\n  suite (AF-001 / #15 / #16) for the exact assertions — a restricted node\n  with both a parent edge and neighbor edges into public nodes, across\n  `restricted`/`confidential`/`unknown` classifications. `public`/`internal`\n  stay indexed.\n- **Opt-in host-predicate filtered (`include`):** restricted units are indexed\n  with their `access` label attached so a host can filter at query time; search\n  still evaluates no principals. `unit.access` is carried for **every**\n  labeled node in this mode (not just ones that happen to match the exclusion\n  criteria — e.g. an explicit `public` label is preserved too), so a host has\n  the full label set to filter on.\n\nExclusion is a pure function of `(label, config)` — no timestamps, no\nrandomness — so artifacts stay byte-identical and the `--check` drift gate stays\ngreen. Override the policy via `AccessExclusionConfig` (`mode`,\n`excludedClassifications`, `excludedVisibilities`).\n\n### Enforcing labels at query time (include-mode)\n\n`include` mode only gets you as far as attaching labels to indexed units —\nsomething still has to check them on every query. `SearchOptions.filterUnit`\nis that hook: an optional `(unit: SearchUnit) => boolean` predicate, applied\nidentically by all three engines (`createSearchEngine`,\n`createLexicalSearchEngine`, and the FAISS path in `createFaissEngine`). A\nunit is only scored/returned when the predicate returns `true`:\n\n```ts\nconst results = await engine.search(query, {\n  filterUnit: (unit) => hasAccess(currentPrincipal, unit.access),\n});\n```\n\n`createSearchServer` exposes the same hook as `ServerConfig.filterUnit`,\nforwarded to every `/search` request — but note it is a **process-wide,\nstatic** predicate set when the server is created, not a per-request one (a\nJSON request body can't carry a function). A host that needs per-request or\nper-principal enforcement should call the library API\n(`createSearchEngine`/`createLexicalSearchEngine`/`createFaissEngine`)\ndirectly and pass a fresh `filterUnit` per call instead of using the bundled\nHTTP server.\n\n**Fails open by design:** a unit whose `access` is `undefined` (no label at\nall) is treated as public unless your `filterUnit` predicate says otherwise —\nthis module never invents a stricter default for unlabeled content. This\nmirrors the index-build behavior in `isExcludedByAccess` (a missing label is\nnever excluded) and is a deliberate, documented choice, not an oversight.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}