montycat 1.1.6 → 1.1.8

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 CHANGED
@@ -14,8 +14,8 @@ The official Node.js & TypeScript SDK for [Montycat](https://montygovernance.com
14
14
  ```typescript
15
15
  // Search your data by MEANING — no external APIs, no separate vector database.
16
16
  // (already ON by default in the montycat-semantic server edition)
17
- const hits = await Sales.semanticSearchGetValues({ query: 'something to listen to music without wires', limitOutput: { start: 0, stop: 5 } });
18
- // → [{ key, score, value: { name: 'Wireless Headphones' } }, ...] (matched by meaning, not keywords)
17
+ const hits = await Sales.semanticSearchGetValues({ query: 'Show all Bluetooth devices', limitOutput: { start: 0, stop: 5 } });
18
+ // → [{ __key__: 123..., __score__: 0.78, __value__: { name: 'Wireless Headphones' }}]
19
19
  ```
20
20
 
21
21
  > ### 🧩 All-in-one. AI-native. **Zero external dependencies.**
@@ -93,7 +93,7 @@ docker run -d --name montycat \
93
93
  -p 21210:21210 -p 21211:21211 \
94
94
  -e MONTYCAT_SUPEROWNER="admin" \
95
95
  -e MONTYCAT_PASSWORD="change-me" \
96
- -v montycat_data:/app/.montycat \
96
+ -v montycat_data:/var/lib/.montycat \
97
97
  montygovernance/montycat:semantic
98
98
  ```
99
99
 
@@ -328,13 +328,13 @@ in the background as data is written (the embedding model is downloaded on deman
328
328
  ```typescript
329
329
  // Semantic search is ON by default in the montycat-semantic edition — just search.
330
330
  // Rank stored items by meaning — two flavors:
331
- // getValues → each hit is { key, score, value }
332
- // getKeys → each hit is { key, score } (lighter; fetch a page later with getBulk)
333
- const hits = await Sales.semanticSearchGetValues({ query: 'something to listen to music without wires', limitOutput: { start: 0, stop: 5 } });
334
- const keys = await Sales.semanticSearchGetKeys({ query: 'something to listen to music without wires', limitOutput: { start: 0, stop: 5 } });
331
+ // getValues → each hit is { __key__, __score__, __value__ }
332
+ // getKeys → each hit is { __key__, __score__ } (lighter; fetch a page later with getBulk)
333
+ const hits = await Sales.semanticSearchGetValues({ query: 'Show all Bluetooth devices', limitOutput: { start: 0, stop: 5 } });
334
+ const keys = await Sales.semanticSearchGetKeys({ query: 'Show all Bluetooth devices', limitOutput: { start: 0, stop: 5 } });
335
335
 
336
336
  // Optionally drop weak matches by cosine similarity (range [-1, 1]).
337
- const strong = await Sales.semanticSearchGetKeys({ query: 'something to listen to music without wires', limitOutput: { start: 0, stop: 5 }, minScore: 0.35 });
337
+ const strong = await Sales.semanticSearchGetKeys({ query: 'Show all Bluetooth devices', limitOutput: { start: 0, stop: 5 }, minScore: 0.35 });
338
338
 
339
339
  // Control the DB-wide switch (optional — it's already on):
340
340
  // switch the embedding model: 'minilm' | 'bge-small' (default) | 'bge-base' | 'e5-small'
@@ -345,6 +345,29 @@ await engine.enableSemanticSearch({ model: 'bge-base' });
345
345
  await engine.disableSemanticSearch();
346
346
  ```
347
347
 
348
+ ### Hybrid semantic search
349
+
350
+ Use semantic ranking with a structured metadata constraint. The filter is a
351
+ hard AND pre-filter using the same criteria shape as `lookupKeysWhere`; it does
352
+ not boost cosine scores.
353
+
354
+ ```typescript
355
+ const matchingKeys = await Sales.semanticSearchGetKeysWhere({
356
+ query: 'astronomy and outer space',
357
+ filters: { category: 'space' },
358
+ limitOutput: { start: 0, stop: 5 },
359
+ minScore: 0.35,
360
+ });
361
+
362
+ const matchingValues = await Sales.semanticSearchGetValuesWhere({
363
+ query: 'astronomy and outer space',
364
+ filters: { category: 'space' },
365
+ limitOutput: { start: 0, stop: 5 },
366
+ });
367
+ // key hits: { __key__, __score__ }
368
+ // value hits: { __key__, __score__, __value__ }
369
+ ```
370
+
348
371
  ## 🔗 Links
349
372
 
350
373
  - 🌐 **Website & Docs** — https://montygovernance.com
@@ -357,4 +380,4 @@ await engine.disableSemanticSearch();
357
380
  - **Is Montycat a vector database or a NoSQL database?** Both — one engine. Store records and query them by *meaning* (vector / semantic search) or by key/schema, without running two systems.
358
381
  - **Do I need OpenAI or an embedding API?** No. Embeddings run on-device in the `montycat-semantic` server. No API keys, no per-query bill, no data egress.
359
382
  - **Is it a Pinecone / Weaviate / Chroma / Qdrant alternative?** Yes — self-hosted and open-source, with a NoSQL store built in.
360
- - **TypeScript support?** First-class — the package ships its own type definitions. Works with Node.js, Deno, Bun, Express, Fastify, and Next.js.
383
+ - **TypeScript support?** First-class — the package ships its own type definitions. Works with Node.js, Deno, Bun, Express, Fastify, and Next.js.
@@ -173,7 +173,7 @@ declare class GenericKV {
173
173
  * (the default) lets the server apply its default top-k (10).
174
174
  * @param minScore - Drop hits whose cosine similarity (in [-1, 1]) is below
175
175
  * this value. Default null (no score filter).
176
- * @return A promise resolving with ranked hits, each `{key, score}`.
176
+ * @return A promise resolving with ranked hits, each `{__key__, __score__}`.
177
177
  */
178
178
  static semanticSearchGetKeys({ query, limitOutput, minScore }: {
179
179
  query: string;
@@ -183,6 +183,36 @@ declare class GenericKV {
183
183
  };
184
184
  minScore?: number | null;
185
185
  }): Promise<any>;
186
+ /**
187
+ * Hybrid semantic search returning ranked keys only, restricted by a
188
+ * metadata filter.
189
+ *
190
+ * Same ranking as `semanticSearchGetKeys`, but only items matching
191
+ * `filters` are considered — a hard AND constraint through the same
192
+ * criteria stack as `lookupKeysWhere` (indexed fields, Timestamp,
193
+ * Pointer). Scores stay pure cosine; the filter never boosts, it only
194
+ * restricts. A filter matching nothing resolves with `[]`.
195
+ *
196
+ * A separate method (not a parameter on `semanticSearchGetKeys`) so
197
+ * existing integrations keep their exact signature.
198
+ *
199
+ * @param query - The natural-language query text to embed and search for.
200
+ * @param filters - Metadata criteria, same shape as `lookupKeysWhere`.
201
+ * @param limitOutput - Start/stop over the ranked hits; `{start: 0, stop: 0}`
202
+ * (the default) lets the server apply its default top-k (10).
203
+ * @param minScore - Drop hits whose cosine similarity (in [-1, 1]) is below
204
+ * this value. Default null (no score filter).
205
+ * @return A promise resolving with ranked hits, each `{__key__, __score__}`.
206
+ */
207
+ static semanticSearchGetKeysWhere({ query, filters, limitOutput, minScore }: {
208
+ query: string;
209
+ filters: object;
210
+ limitOutput?: {
211
+ start: number;
212
+ stop: number;
213
+ };
214
+ minScore?: number | null;
215
+ }): Promise<any>;
186
216
  /**
187
217
  * Semantic (vector similarity) search returning ranked hits with their values.
188
218
  *
@@ -204,7 +234,9 @@ declare class GenericKV {
204
234
  * returned value.
205
235
  * @param pointersMetadata - Whether to include pointer metadata in each
206
236
  * returned value.
207
- * @return A promise resolving with ranked hits, each `{key, score, value}`.
237
+ * @return A promise resolving with ranked hits, each
238
+ * `{__key__, __score__, __value__}` — the same dunder envelope
239
+ * `lookupValuesWhere` returns with `keyIncluded: true`, plus the score.
208
240
  */
209
241
  static semanticSearchGetValues({ query, limitOutput, minScore, withPointers, pointersMetadata }: {
210
242
  query: string;
@@ -216,6 +248,44 @@ declare class GenericKV {
216
248
  withPointers?: boolean;
217
249
  pointersMetadata?: boolean;
218
250
  }): Promise<any>;
251
+ /**
252
+ * Hybrid semantic search returning ranked hits with their values,
253
+ * restricted by a metadata filter.
254
+ *
255
+ * Same ranking as `semanticSearchGetValues`, but only items matching
256
+ * `filters` are considered — a hard AND constraint through the same
257
+ * criteria stack as `lookupKeysWhere` (indexed fields, Timestamp,
258
+ * Pointer). Scores stay pure cosine; the filter never boosts, it only
259
+ * restricts. A filter matching nothing resolves with `[]`.
260
+ *
261
+ * A separate method (not a parameter on `semanticSearchGetValues`) so
262
+ * existing integrations keep their exact signature.
263
+ *
264
+ * @param query - The natural-language query text to embed and search for.
265
+ * @param filters - Metadata criteria, same shape as `lookupKeysWhere`.
266
+ * @param limitOutput - Start/stop over the ranked hits; `{start: 0, stop: 0}`
267
+ * (the default) lets the server apply its default top-k (10).
268
+ * @param minScore - Drop hits whose cosine similarity (in [-1, 1]) is below
269
+ * this value. Default null (no score filter).
270
+ * @param withPointers - Whether to include pointers (foreign values) in each
271
+ * returned value.
272
+ * @param pointersMetadata - Whether to include pointer metadata in each
273
+ * returned value.
274
+ * @return A promise resolving with ranked hits, each
275
+ * `{__key__, __score__, __value__}` — the same dunder envelope
276
+ * `lookupValuesWhere` returns with `keyIncluded: true`, plus the score.
277
+ */
278
+ static semanticSearchGetValuesWhere({ query, filters, limitOutput, minScore, withPointers, pointersMetadata }: {
279
+ query: string;
280
+ filters: object;
281
+ limitOutput?: {
282
+ start: number;
283
+ stop: number;
284
+ };
285
+ minScore?: number | null;
286
+ withPointers?: boolean;
287
+ pointersMetadata?: boolean;
288
+ }): Promise<any>;
219
289
  /**
220
290
  * Gets the length of the keyspace.
221
291
  * @returns A promise that resolves with the length of the keyspace.
@@ -221,7 +221,7 @@ class GenericKV {
221
221
  * public methods differ only in which value-inclusion flags they pass,
222
222
  * so the wire call lives here once.
223
223
  */
224
- static async semanticSearchCore(query, limitOutput, minScore, withPointers, keyIncluded, pointersMetadata) {
224
+ static async semanticSearchCore(query, limitOutput, minScore, filters, withPointers, keyIncluded, pointersMetadata) {
225
225
  if (!query || !query.trim()) {
226
226
  throw new Error("No query text provided for semantic search.");
227
227
  }
@@ -230,6 +230,7 @@ class GenericKV {
230
230
  semanticQuery: query,
231
231
  limitOutput,
232
232
  minScore,
233
+ semanticFilter: filters,
233
234
  withPointers,
234
235
  keyIncluded,
235
236
  pointersMetadata,
@@ -254,10 +255,37 @@ class GenericKV {
254
255
  * (the default) lets the server apply its default top-k (10).
255
256
  * @param minScore - Drop hits whose cosine similarity (in [-1, 1]) is below
256
257
  * this value. Default null (no score filter).
257
- * @return A promise resolving with ranked hits, each `{key, score}`.
258
+ * @return A promise resolving with ranked hits, each `{__key__, __score__}`.
258
259
  */
259
260
  static async semanticSearchGetKeys({ query, limitOutput = { start: 0, stop: 0 }, minScore = null }) {
260
- return this.semanticSearchCore(query, limitOutput, minScore, false, false, false);
261
+ return this.semanticSearchCore(query, limitOutput, minScore, null, false, false, false);
262
+ }
263
+ /**
264
+ * Hybrid semantic search returning ranked keys only, restricted by a
265
+ * metadata filter.
266
+ *
267
+ * Same ranking as `semanticSearchGetKeys`, but only items matching
268
+ * `filters` are considered — a hard AND constraint through the same
269
+ * criteria stack as `lookupKeysWhere` (indexed fields, Timestamp,
270
+ * Pointer). Scores stay pure cosine; the filter never boosts, it only
271
+ * restricts. A filter matching nothing resolves with `[]`.
272
+ *
273
+ * A separate method (not a parameter on `semanticSearchGetKeys`) so
274
+ * existing integrations keep their exact signature.
275
+ *
276
+ * @param query - The natural-language query text to embed and search for.
277
+ * @param filters - Metadata criteria, same shape as `lookupKeysWhere`.
278
+ * @param limitOutput - Start/stop over the ranked hits; `{start: 0, stop: 0}`
279
+ * (the default) lets the server apply its default top-k (10).
280
+ * @param minScore - Drop hits whose cosine similarity (in [-1, 1]) is below
281
+ * this value. Default null (no score filter).
282
+ * @return A promise resolving with ranked hits, each `{__key__, __score__}`.
283
+ */
284
+ static async semanticSearchGetKeysWhere({ query, filters, limitOutput = { start: 0, stop: 0 }, minScore = null }) {
285
+ if (!filters || Object.keys(filters).length === 0) {
286
+ throw new Error("No filters provided for hybrid semantic search.");
287
+ }
288
+ return this.semanticSearchCore(query, limitOutput, minScore, filters, false, false, false);
261
289
  }
262
290
  /**
263
291
  * Semantic (vector similarity) search returning ranked hits with their values.
@@ -280,10 +308,45 @@ class GenericKV {
280
308
  * returned value.
281
309
  * @param pointersMetadata - Whether to include pointer metadata in each
282
310
  * returned value.
283
- * @return A promise resolving with ranked hits, each `{key, score, value}`.
311
+ * @return A promise resolving with ranked hits, each
312
+ * `{__key__, __score__, __value__}` — the same dunder envelope
313
+ * `lookupValuesWhere` returns with `keyIncluded: true`, plus the score.
284
314
  */
285
315
  static async semanticSearchGetValues({ query, limitOutput = { start: 0, stop: 0 }, minScore = null, withPointers = false, pointersMetadata = false }) {
286
- return this.semanticSearchCore(query, limitOutput, minScore, withPointers, true, pointersMetadata);
316
+ return this.semanticSearchCore(query, limitOutput, minScore, null, withPointers, true, pointersMetadata);
317
+ }
318
+ /**
319
+ * Hybrid semantic search returning ranked hits with their values,
320
+ * restricted by a metadata filter.
321
+ *
322
+ * Same ranking as `semanticSearchGetValues`, but only items matching
323
+ * `filters` are considered — a hard AND constraint through the same
324
+ * criteria stack as `lookupKeysWhere` (indexed fields, Timestamp,
325
+ * Pointer). Scores stay pure cosine; the filter never boosts, it only
326
+ * restricts. A filter matching nothing resolves with `[]`.
327
+ *
328
+ * A separate method (not a parameter on `semanticSearchGetValues`) so
329
+ * existing integrations keep their exact signature.
330
+ *
331
+ * @param query - The natural-language query text to embed and search for.
332
+ * @param filters - Metadata criteria, same shape as `lookupKeysWhere`.
333
+ * @param limitOutput - Start/stop over the ranked hits; `{start: 0, stop: 0}`
334
+ * (the default) lets the server apply its default top-k (10).
335
+ * @param minScore - Drop hits whose cosine similarity (in [-1, 1]) is below
336
+ * this value. Default null (no score filter).
337
+ * @param withPointers - Whether to include pointers (foreign values) in each
338
+ * returned value.
339
+ * @param pointersMetadata - Whether to include pointer metadata in each
340
+ * returned value.
341
+ * @return A promise resolving with ranked hits, each
342
+ * `{__key__, __score__, __value__}` — the same dunder envelope
343
+ * `lookupValuesWhere` returns with `keyIncluded: true`, plus the score.
344
+ */
345
+ static async semanticSearchGetValuesWhere({ query, filters, limitOutput = { start: 0, stop: 0 }, minScore = null, withPointers = false, pointersMetadata = false }) {
346
+ if (!filters || Object.keys(filters).length === 0) {
347
+ throw new Error("No filters provided for hybrid semantic search.");
348
+ }
349
+ return this.semanticSearchCore(query, limitOutput, minScore, filters, withPointers, true, pointersMetadata);
287
350
  }
288
351
  /**
289
352
  * Gets the length of the keyspace.
@@ -58,7 +58,7 @@ function processBulkKeysValues(bulkKeysValues) {
58
58
  * @returns A JSON string representing the binary query.
59
59
  * */
60
60
  function convertToBinaryQuery(cls, options = {}) {
61
- const { key = null, value = {}, expireSec = 0, bulkValues = [], bulkKeys = [], bulkKeysValues = {}, searchCriteria = {}, limitOutput = { start: 0, stop: 0 }, withPointers = false, schema = null, keyIncluded = false, pointersMetadata = false, volumes = [], latestVolume = false, semanticQuery = null, minScore = null, waitForIndex = null, } = options;
61
+ const { key = null, value = {}, expireSec = 0, bulkValues = [], bulkKeys = [], bulkKeysValues = {}, searchCriteria = {}, limitOutput = { start: 0, stop: 0 }, withPointers = false, schema = null, keyIncluded = false, pointersMetadata = false, volumes = [], latestVolume = false, semanticQuery = null, minScore = null, semanticFilter = null, waitForIndex = null, } = options;
62
62
  const { processedValue, foundSchema } = processValue(value);
63
63
  const { processedBulkValues, uniqueSchema } = processBulkValues(bulkValues);
64
64
  const bulkKeysValuesProcessed = processBulkKeysValues(bulkKeysValues);
@@ -94,6 +94,12 @@ function convertToBinaryQuery(cls, options = {}) {
94
94
  if (minScore !== null) {
95
95
  queryDict.min_score = minScore;
96
96
  }
97
+ // Hybrid metadata pre-filter for `semantic_search` (hard AND constraint,
98
+ // same criteria shape as lookupKeysWhere — Timestamp/Pointer supported).
99
+ // Omit when null so the wire is unchanged for existing commands.
100
+ if (semanticFilter !== null) {
101
+ queryDict.semantic_filter = JSON.stringify(processSearchCriteria(semanticFilter));
102
+ }
97
103
  // Per-request wait_for_index override for persistent writes; omit when null
98
104
  // so the server falls back to its DB-wide default (existing wire unchanged).
99
105
  if (waitForIndex !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "montycat",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Self-hosted vector database + NoSQL with built-in AI semantic search — the Node.js & TypeScript client for Montycat. A Rust-powered, AI-native Pinecone / Weaviate / Chroma alternative for RAG, AI agents & LLM memory.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",