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 +274 -0
- package/dist/index.cjs +480 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +230 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.js +465 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# nx-semantic-matcher
|
|
2
|
+
|
|
3
|
+
**Tiered Text Matching Pipeline for TypeScript/Node.js**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/nx-semantic-matcher)
|
|
6
|
+
[](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
|