Skip to content

Commit 43ec49e

Browse files
committed
feat: v8.0.0 — add search modes (semantic/lexical/hybrid) and min_similarity
1 parent 3445b99 commit 43ec49e

5 files changed

Lines changed: 176 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,41 @@ adheres to [Semantic Versioning][semver].
77
[kac]: https://keepachangelog.com/en/1.1.0/
88
[semver]: https://semver.org/
99

10+
## 8.0.0
11+
12+
### Breaking changes
13+
14+
- None. `mode` and `minSimilarity` are both optional; existing
15+
`searchSemantic` callers continue to work unchanged. Major bump
16+
reflects the conceptual expansion of the search contract (three
17+
retrieval modes instead of one), not API-breaking removals.
18+
19+
### Added
20+
21+
- `KoolbaseDatabase.searchSemantic` accepts a new `mode` parameter of
22+
type `SearchMode`. Three retrieval strategies are supported:
23+
- `'semantic'` (default) — pure vector search via HNSW on cosine
24+
distance. Best for fuzzy / conceptual queries.
25+
- `'lexical'` — pure BM25 over the field's source text via Postgres
26+
`ts_rank_cd`. Best for exact terms, codes, names, acronyms.
27+
- `'hybrid'` — vector + lexical fused with reciprocal rank fusion
28+
(k=60). Generally the strongest default for production search.
29+
- `KoolbaseDatabase.searchSemantic` accepts a new `minSimilarity`
30+
parameter (0..100, optional). Server-side filter that drops results
31+
below the given similarity percentage before they cross the wire.
32+
Saves bandwidth on weak matches. Only valid for semantic and hybrid
33+
modes; the server rejects it on lexical mode (BM25 ranks aren't
34+
comparable to cosine similarity).
35+
- New `SearchMode` type exported from `@techfinityedge/koolbase-react-native`.
36+
37+
### Server requirements
38+
39+
- Requires Koolbase API release with hybrid search shipped (June 8 2026
40+
or later).
41+
- Lexical and hybrid modes require the vector field to have a
42+
`source_field` configured. The lexical sidecar table populates
43+
automatically on record write via the same hook that drives auto-embed.
44+
1045
## 7.0.0
1146

1247
### Breaking changes

README.md

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -379,37 +379,90 @@ When the device is offline, these writes are queued and synced automatically whe
379379

380380
---
381381

382-
### Semantic search
382+
### Semantic, lexical, and hybrid search
383383

384-
Find records by meaning, not just field equality. Two paths: let Koolbase
385-
embed text for you on the server (recommended — no client-side model
386-
needed), or pass a precomputed vector.
384+
Find records by meaning, exact terms, or both. Koolbase ships three
385+
retrieval modes from a single API — pick the one that matches your
386+
query characteristics, or use `'hybrid'` as a strong production default.
387387

388388
Declare a vector field on the collection from the dashboard or CLI first
389389
(picking a dimension; v1 supports 384, 768, 1024, and 1536).
390390

391-
**Server-side embedding (recommended).** Configure an AI provider on the
392-
project once (Gemini's free tier works; OpenAI also supported), tag the
393-
vector field with the provider/model/source_field, and Koolbase
394-
auto-embeds records as they're inserted or updated:
391+
#### The three search modes
395392

396393
```typescript
397-
// One-time setup via dashboard. Then just write records normally:
398-
await Koolbase.db.insert({
394+
// Semantic (default) — pure vector search via HNSW + cosine. Best for
395+
// fuzzy or conceptual queries where exact words don't have to match.
396+
const result = await Koolbase.db.searchSemantic({
399397
collection: 'articles',
400-
data: {
401-
title: 'How to ship faster',
402-
content: 'Cut scope ruthlessly. Ship the smallest useful slice...',
403-
},
398+
field: 'content_embedding',
399+
queryText: 'how do I move quicker?',
400+
limit: 10,
404401
});
405402

406-
// Query by text — server embeds inline using the configured provider:
403+
// Lexical — pure BM25 over the field's source text (Postgres
404+
// ts_rank_cd). Best for exact terms, product codes, names, acronyms.
405+
const result = await Koolbase.db.searchSemantic({
406+
collection: 'articles',
407+
field: 'content_embedding',
408+
queryText: 'CVE-2024-1234',
409+
mode: 'lexical',
410+
limit: 10,
411+
});
412+
413+
// Hybrid — vector + lexical fused with reciprocal rank fusion (k=60).
414+
// Generally the strongest default; both rankers vote and the fused
415+
// score promotes records that score well on either signal.
416+
const result = await Koolbase.db.searchSemantic({
417+
collection: 'articles',
418+
field: 'content_embedding',
419+
queryText: 'production deploy pipeline',
420+
mode: 'hybrid',
421+
limit: 10,
422+
});
423+
```
424+
425+
#### Filtering weak matches
426+
427+
For `'semantic'` and `'hybrid'` modes, pass `minSimilarity` (0..100) to
428+
drop results below a similarity threshold server-side — saves bandwidth
429+
on weak matches:
430+
431+
```typescript
407432
const result = await Koolbase.db.searchSemantic({
408433
collection: 'articles',
409434
field: 'content_embedding',
410435
queryText: 'how do I move quicker?',
436+
mode: 'hybrid',
437+
minSimilarity: 70, // only matches at least 70% similar
411438
limit: 10,
412439
});
440+
```
441+
442+
`minSimilarity` is rejected by the server when used with `'lexical'`
443+
BM25 rank scores aren't comparable to cosine similarity, and silently
444+
ignoring the parameter would produce confusing behavior.
445+
446+
#### Server-side embedding (recommended)
447+
448+
Configure an AI provider on the project once (Gemini's free tier works;
449+
OpenAI also supported), tag the vector field with the
450+
provider/model/source_field, and Koolbase auto-embeds records as
451+
they're inserted or updated. Lexical indexing happens automatically on
452+
the same write, so all three search modes work without extra setup:
453+
454+
```typescript
455+
// One-time setup via dashboard. Then just write records normally —
456+
// vectors AND lexical rows land within ~1s.
457+
await Koolbase.db.insert({
458+
collection: 'articles',
459+
data: {
460+
title: 'How to ship faster',
461+
content: 'Cut scope ruthlessly. Ship the smallest useful slice...',
462+
},
463+
});
464+
465+
// Iterate over hits the same way regardless of mode:
413466
for (const hit of result.hits) {
414467
console.log(`${hit.record.data.title} ${hit.distance.toFixed(3)}`);
415468
}
@@ -430,8 +483,11 @@ await Koolbase.db.embedText({
430483
});
431484
```
432485

433-
**Client-side embedding (advanced).** If you'd rather control the
434-
embedding model yourself, pass a vector instead of text:
486+
#### Client-side embedding (advanced)
487+
488+
If you'd rather control the embedding model yourself, pass a vector
489+
instead of text. Note that lexical and hybrid modes require text, since
490+
BM25 has no notion of "vector queries":
435491

436492
```typescript
437493
// Set a vector you've encoded yourself
@@ -445,7 +501,7 @@ await Koolbase.db.setVector(
445501
const v = await Koolbase.db.getVector(articleId, 'embedding');
446502
console.log(`${v.vector.length}-dim, updated ${v.updatedAt}`);
447503

448-
// Search with a precomputed vector
504+
// Search with a precomputed vector — semantic mode only.
449505
const result = await Koolbase.db.searchSemantic({
450506
collection: 'articles',
451507
field: 'embedding',
@@ -458,25 +514,32 @@ const result = await Koolbase.db.searchSemantic({
458514
await Koolbase.db.deleteVector(articleId, 'embedding');
459515
```
460516

461-
A few behaviors worth knowing:
517+
#### Behaviors worth knowing
462518

463519
- **Pass exactly one of `queryVector` or `queryText`.** Supplying both
464520
or neither throws an `Error`.
521+
- **`queryVector` is for semantic mode only.** Lexical and hybrid need
522+
raw text — the server uses it for BM25 ranking (and embeds it inline
523+
for the vector half of hybrid).
465524
- **Vector length must match the declared dimension.** Mismatches throw
466525
`KoolbaseVectorDimensionMismatchError`.
526+
- **`minSimilarity` must be 0..100.** Values outside that range throw
527+
an `Error` client-side before the request is sent.
467528
- **Online-only.** Vector operations are not cached locally or queued
468-
offline — HNSW similarity search has no useful offline semantics.
529+
offline — HNSW similarity and BM25 ranking have no useful offline
530+
semantics.
469531
- **Read rule applies post-search.** `owner`/`scoped`/`conditional`
470-
records are filtered to the caller after the HNSW lookup, so strict
471-
rules may return fewer than `limit` results.
532+
records are filtered to the caller after retrieval, so strict rules
533+
may return fewer than `limit` results.
472534
- **`embedText` is async.** Returns when the job is queued (~100ms).
473535
The vector lands within 1 second once the worker picks it up.
474536
- **Higher dimensions coming.** `text-embedding-3-large` (3072 dim)
475-
supported once pgvector is upgraded. Use `dimensions=1536` Matryoshka
476-
truncation in the meantime.
537+
supported once pgvector is upgraded. Use `dimensions=1536`
538+
Matryoshka truncation in the meantime.
477539

478540
See [Semantic search docs](https://docs.koolbase.com/database/vectors)
479-
for setup, provider configuration, and embedding model recommendations.
541+
for setup, provider configuration, embedding model recommendations,
542+
and when to pick each mode.
480543

481544
---
482545

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@techfinityedge/koolbase-react-native",
3-
"version": "7.0.0",
3+
"version": "8.0.0",
44
"description": "React Native SDK for Koolbase — auth, database, storage, realtime, feature flags, and functions in one package.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/database.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
BatchResult,
99
KoolbaseVector,
1010
SemanticSearchResult,
11+
SearchMode,
1112
} from './types';
1213
import {
1314
getCached,
@@ -547,11 +548,8 @@ export class KoolbaseDatabase {
547548
await this.request<{ queued: boolean }>('POST', '/v1/sdk/db/embed-text', body);
548549
}
549550

550-
551551
/**
552-
* Semantic search over a vector field. Supply EITHER `queryVector`
553-
* (precomputed) OR `queryText` (the server embeds it inline using the
554-
* vector field's configured provider).
552+
* Search for records based on their semantic similarity to a query.
555553
*
556554
* @example
557555
* // Server-side embedding — most common:
@@ -569,6 +567,25 @@ export class KoolbaseDatabase {
569567
* queryVector: precomputed,
570568
* limit: 10,
571569
* });
570+
*
571+
* // Hybrid search (vector + BM25, RRF-fused):
572+
* const result = await Koolbase.db.searchSemantic({
573+
* collection: 'articles',
574+
* field: 'content_embedding',
575+
* queryText: 'how do I configure CI/CD?',
576+
* mode: 'hybrid',
577+
* minSimilarity: 70,
578+
* });
579+
*
580+
* `mode` selects the retrieval strategy:
581+
* - `'semantic'` (default) — pure vector search via HNSW
582+
* - `'lexical'` — pure BM25 over the field's source text
583+
* - `'hybrid'` — vector + lexical, RRF-fused (k=60)
584+
*
585+
* `minSimilarity` (0..100, optional) filters out results below the
586+
* given similarity percentage server-side. Saves bandwidth on weak
587+
* matches. Only valid for semantic and hybrid; rejected by the
588+
* server on lexical mode.
572589
*/
573590
async searchSemantic(opts: {
574591
collection: string;
@@ -577,6 +594,8 @@ export class KoolbaseDatabase {
577594
queryText?: string;
578595
limit?: number;
579596
where?: Record<string, unknown>;
597+
mode?: SearchMode;
598+
minSimilarity?: number;
580599
}): Promise<SemanticSearchResult> {
581600
const hasVector = Array.isArray(opts.queryVector) && opts.queryVector.length > 0;
582601
const hasText = typeof opts.queryText === 'string' && opts.queryText.trim().length > 0;
@@ -586,17 +605,32 @@ export class KoolbaseDatabase {
586605
if (hasVector && hasText) {
587606
throw new Error('searchSemantic: provide only one of queryVector or queryText.');
588607
}
589-
608+
if (
609+
opts.minSimilarity !== undefined &&
610+
(opts.minSimilarity < 0 || opts.minSimilarity > 100)
611+
) {
612+
throw new Error(
613+
`searchSemantic: minSimilarity must be between 0 and 100, got ${opts.minSimilarity}.`,
614+
);
615+
}
590616
const body: Record<string, unknown> = {
591617
collection: opts.collection,
592618
field: opts.field,
593619
limit: opts.limit ?? 20,
620+
// Always send mode so the server uses the SDK's intent rather
621+
// than its own default. Omitting for 'semantic' would also work
622+
// (server defaults to semantic) but explicit is safer if the
623+
// server's default ever shifts.
624+
mode: opts.mode ?? 'semantic',
594625
};
595626
if (hasVector) body.query_vector = opts.queryVector;
596627
if (hasText) body.query_text = opts.queryText;
597628
if (opts.where && Object.keys(opts.where).length > 0) {
598629
body.where = opts.where;
599630
}
631+
if (opts.minSimilarity !== undefined) {
632+
body.min_similarity = opts.minSimilarity;
633+
}
600634
const raw = await this.request<{
601635
results: Array<{ record: Record<string, unknown>; distance: number }>;
602636
total: number;

src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,19 @@ export interface KoolbaseVector {
190190
updatedAt: string;
191191
}
192192

193+
/**
194+
* Retrieval strategy for `KoolbaseDatabase.searchSemantic()`.
195+
*
196+
* - `'semantic'` (default) — pure vector search via HNSW on cosine
197+
* distance. Best for fuzzy / conceptual queries where exact term
198+
* match isn't required.
199+
* - `'lexical'` — pure BM25 over the field's source text (Postgres
200+
* `ts_rank_cd`). Best for exact terms, codes, names, acronyms.
201+
* - `'hybrid'` — vector + lexical fused with reciprocal rank fusion
202+
* (k=60). Generally the strongest default for production search.
203+
*/
204+
export type SearchMode = 'semantic' | 'lexical' | 'hybrid';
205+
193206
/**
194207
* One ranked hit from `KoolbaseDatabase.searchSemantic()`. `record` is
195208
* the full record (same wire shape as a record returned by query/get).

0 commit comments

Comments
 (0)