nx-semantic-matcher 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # nx-semantic-matcher
2
+
3
+ **Tiered Text Matching Pipeline for TypeScript/Node.js**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/nx-semantic-matcher.svg)](https://www.npmjs.com/package/nx-semantic-matcher)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ Match an input string against a list of candidate items using a 4-tier pipeline — each tier is progressively smarter and more expensive. The pipeline stops as soon as a confident match is found.
9
+
10
+ | Tier | Name | Speed | Cost | What it handles |
11
+ |------|------|-------|------|-----------------|
12
+ | T1 | Exact / Normalized | ~0 ms | Free | Case, whitespace, punctuation differences |
13
+ | T2 | Fuzzy (Fuse.js) | ~1–5 ms | Free | Typos, minor reordering, character transpositions |
14
+ | T3 | Semantic Embeddings | ~10–80 ms | Free (local) / ~$0.02/M tokens (OpenAI) | Synonyms, paraphrasing, intent equivalence |
15
+ | T4 | LLM Classification | ~500–3000 ms | Pay-per-call | Ambiguous edge cases — last resort |
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install nx-semantic-matcher
23
+ ```
24
+
25
+ Install optional providers for the tiers you need:
26
+
27
+ ```bash
28
+ npm install @xenova/transformers # Tier 3 – local embeddings (no API key, ~30 MB)
29
+ npm install openai # Tier 3 OpenAI embeddings OR Tier 4 OpenAI LLM
30
+ npm install @anthropic-ai/sdk # Tier 4 Anthropic LLM
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ```typescript
38
+ import { SemanticMatcher } from "nx-semantic-matcher";
39
+
40
+ const questions = [
41
+ { id: "q1", text: "How do I reset my password?" },
42
+ { id: "q2", text: "What is your refund policy?" },
43
+ { id: "q3", text: "How do I contact support?" },
44
+ ];
45
+
46
+ // Local embeddings — no API key needed, downloads ~30 MB on first run
47
+ const matcher = new SemanticMatcher(questions, {
48
+ embedding: { provider: "local" },
49
+ });
50
+
51
+ const result = await matcher.match("Steps to change my password");
52
+
53
+ if (result.found) {
54
+ console.log(`Matched: ${result.id} via Tier ${result.tier} (score ${result.score})`);
55
+ // → Matched: q1 via Tier 3 (score 0.87)
56
+ } else {
57
+ console.log(`Not found: ${result.reason}`);
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Configuration
64
+
65
+ ```typescript
66
+ import { SemanticMatcher, MatcherConfig } from "nx-semantic-matcher";
67
+
68
+ const config: MatcherConfig = {
69
+ // Tier toggles and thresholds
70
+ tiers: {
71
+ t1: { enabled: true },
72
+ t2: { enabled: true, threshold: 0.72 },
73
+ t3: { enabled: true, threshold: 0.72, lazy: false },
74
+ t4: { enabled: false, threshold: "medium", maxCandidatesInPrompt: 100, timeout: 10000 },
75
+ },
76
+
77
+ // Embedding provider for T3
78
+ embedding: {
79
+ provider: "local", // "local" | "openai" | EmbeddingProvider
80
+ // model: "Xenova/all-MiniLM-L6-v2",
81
+ // apiKey: "sk-...",
82
+ },
83
+
84
+ // LLM provider for T4 (only needed when t4.enabled is true)
85
+ llm: {
86
+ provider: "anthropic", // "anthropic" | "openai" | LLMProvider
87
+ // model: "claude-3-5-haiku-20241022",
88
+ // apiKey: "...",
89
+ },
90
+
91
+ debug: false, // log tier decisions to stderr
92
+ };
93
+ ```
94
+
95
+ ### Configuration Quick Reference
96
+
97
+ | Config key | Default | Description |
98
+ |---|---|---|
99
+ | `tiers.t1.enabled` | `true` | Enable Tier 1 exact/normalized matching |
100
+ | `tiers.t2.enabled` | `true` | Enable Tier 2 Fuse.js fuzzy matching |
101
+ | `tiers.t2.threshold` | `0.72` | Min confidence for T2 match (0–1) |
102
+ | `tiers.t3.enabled` | `true` | Enable Tier 3 embedding similarity |
103
+ | `tiers.t3.threshold` | `0.72` | Min cosine similarity for T3 match |
104
+ | `tiers.t3.lazy` | `false` | Defer index build to first query |
105
+ | `tiers.t4.enabled` | `false` | Enable Tier 4 LLM classification |
106
+ | `tiers.t4.threshold` | `"medium"` | Min LLM confidence: `"high"` or `"medium"` |
107
+ | `tiers.t4.maxCandidatesInPrompt` | `100` | Max items sent to LLM |
108
+ | `tiers.t4.timeout` | `10000` | LLM timeout in ms |
109
+ | `embedding.provider` | — | `"local"` \| `"openai"` \| `EmbeddingProvider` |
110
+ | `llm.provider` | — | `"anthropic"` \| `"openai"` \| `LLMProvider` |
111
+ | `debug` | `false` | Log tier decisions to stderr |
112
+
113
+ ---
114
+
115
+ ## API
116
+
117
+ ### `new SemanticMatcher(items, config?)`
118
+
119
+ Creates a new matcher. Builds the T3 embedding index eagerly unless `tiers.t3.lazy = true`.
120
+
121
+ ```typescript
122
+ const matcher = new SemanticMatcher(
123
+ [{ id: "1", text: "..." }],
124
+ { embedding: { provider: "local" } }
125
+ );
126
+ ```
127
+
128
+ ### `matcher.match(query)`
129
+
130
+ Matches a query against the current item list.
131
+
132
+ ```typescript
133
+ const result = await matcher.match("my query");
134
+ // result: MatchFound | MatchNotFound
135
+ ```
136
+
137
+ ### `matcher.setItems(items)`
138
+
139
+ Replaces the candidate list and rebuilds the embedding index.
140
+
141
+ ### `matcher.rebuildIndex()`
142
+
143
+ Force-rebuilds the T3 index (e.g. after external mutation).
144
+
145
+ ### `matcher.dispose()`
146
+
147
+ Releases model handles and clears in-memory vectors.
148
+
149
+ ### `SemanticMatcher.matchOnce(query, items, config?)`
150
+
151
+ Static convenience method — creates, matches, and disposes in one call. Avoid in hot loops.
152
+
153
+ ---
154
+
155
+ ## Usage Examples
156
+
157
+ ### Fuzzy-only (no AI dependencies)
158
+
159
+ ```typescript
160
+ const matcher = new SemanticMatcher(items, {
161
+ tiers: {
162
+ t3: { enabled: false },
163
+ t4: { enabled: false },
164
+ },
165
+ });
166
+ // T1 + T2 only — zero model downloads, synchronous-equivalent
167
+ ```
168
+
169
+ ### With LLM fallback (OpenAI)
170
+
171
+ ```typescript
172
+ const matcher = new SemanticMatcher(items, {
173
+ embedding: { provider: "openai", apiKey: process.env.OPENAI_API_KEY },
174
+ tiers: {
175
+ t4: {
176
+ enabled: true,
177
+ provider: "openai",
178
+ threshold: "high",
179
+ },
180
+ },
181
+ });
182
+ ```
183
+
184
+ ### Custom embedding provider
185
+
186
+ ```typescript
187
+ import type { EmbeddingProvider } from "nx-semantic-matcher";
188
+
189
+ class MyProvider implements EmbeddingProvider {
190
+ async embed(text: string): Promise<Float32Array> { /* ... */ }
191
+ async embedBatch(texts: string[]): Promise<Float32Array[]> { /* ... */ }
192
+ }
193
+
194
+ const matcher = new SemanticMatcher(items, {
195
+ tiers: { t3: { provider: new MyProvider() } },
196
+ });
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Provider Interfaces
202
+
203
+ ### `EmbeddingProvider`
204
+
205
+ ```typescript
206
+ interface EmbeddingProvider {
207
+ embed(text: string): Promise<Float32Array>;
208
+ embedBatch?(texts: string[]): Promise<Float32Array[]>;
209
+ init?(): Promise<void>;
210
+ dispose?(): Promise<void>;
211
+ }
212
+ ```
213
+
214
+ ### `LLMProvider`
215
+
216
+ ```typescript
217
+ interface LLMProvider {
218
+ classify(query: string, candidates: MatchItem[]): Promise<LLMClassification>;
219
+ }
220
+ ```
221
+
222
+ ---
223
+
224
+ ## Result Types
225
+
226
+ ```typescript
227
+ type MatchResult = MatchFound | MatchNotFound;
228
+
229
+ interface MatchFound {
230
+ found: true;
231
+ id: string; // matched item id
232
+ text: string; // matched item text
233
+ score: number; // confidence [0, 1]
234
+ tier: 1 | 2 | 3 | 4; // which tier matched
235
+ tierName: string; // human-readable tier label
236
+ durationMs: number; // total pipeline duration
237
+ reasoning?: string; // populated only for tier 4
238
+ }
239
+
240
+ interface MatchNotFound {
241
+ found: false;
242
+ durationMs: number;
243
+ reason: string;
244
+ }
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Error Handling
250
+
251
+ `nx-semantic-matcher` **never throws for a failed match** — it returns `MatchNotFound`. It **does throw** for misconfiguration:
252
+
253
+ | Error | Thrown when |
254
+ |---|---|
255
+ | `NxConfigError` | Invalid config at construction time |
256
+ | `NxProviderError` | Provider init fails (bad API key, missing package) |
257
+ | `NxIndexError` | Embedding index build or query fails |
258
+
259
+ Tier 4 LLM errors (timeout, API 5xx, JSON parse failure) are caught silently — they log a warning (if `debug: true`) and the pipeline returns `NOT_FOUND`.
260
+
261
+ ---
262
+
263
+ ## Performance
264
+
265
+ - **T1 + T2**: `< 5 ms` for up to 100,000 items.
266
+ - **T3 index build**: ~50 ms per 1,000 items with the local model — run eagerly at startup.
267
+ - **T3 query**: O(n·d) cosine scan. For n=10,000, d=384: ~15 ms on a modern CPU.
268
+ - For n > 50,000 or sub-10 ms T3 requirements: plug in a vector database (pgvector, Qdrant) via a custom `EmbeddingProvider`.
269
+
270
+ ---
271
+
272
+ ## License
273
+
274
+ MIT