montycat 1.1.7 → 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
@@ -15,7 +15,7 @@ The official Node.js & TypeScript SDK for [Montycat](https://montygovernance.com
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
17
  const hits = await Sales.semanticSearchGetValues({ query: 'Show all Bluetooth devices', limitOutput: { start: 0, stop: 5 } });
18
- // → [{ __key__, __score__, __value__: { name: 'Wireless Headphones' } }, ...] (matched by meaning, not keywords)
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
 
@@ -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.
@@ -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
  *
@@ -218,6 +248,44 @@ declare class GenericKV {
218
248
  withPointers?: boolean;
219
249
  pointersMetadata?: boolean;
220
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>;
221
289
  /**
222
290
  * Gets the length of the keyspace.
223
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,
@@ -257,7 +258,34 @@ class GenericKV {
257
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.
@@ -285,7 +313,40 @@ class GenericKV {
285
313
  * `lookupValuesWhere` returns with `keyIncluded: true`, plus the score.
286
314
  */
287
315
  static async semanticSearchGetValues({ query, limitOutput = { start: 0, stop: 0 }, minScore = null, withPointers = false, pointersMetadata = false }) {
288
- 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);
289
350
  }
290
351
  /**
291
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.7",
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",