Skip to content

Commit dab8ef1

Browse files
authored
Merge pull request #5 from FoxNoseTech/feat/add-vector-fields-support
feat: vector search support for Flux API (v0.3.0)
2 parents dd58844 + 0aeb42c commit dab8ef1

8 files changed

Lines changed: 980 additions & 4 deletions

File tree

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,64 @@ console.log(schema.searchable_fields);
100100
client.close();
101101
```
102102

103+
### Vector Search
104+
105+
The Flux client provides typed convenience methods for all vector search modes:
106+
107+
```typescript
108+
import { FluxClient, SearchMode, buildSearchBody } from '@foxnose/sdk';
109+
110+
// Semantic search (auto-generated embeddings)
111+
const results = await client.vectorSearch('articles', {
112+
query: 'machine learning in healthcare',
113+
top_k: 10,
114+
similarity_threshold: 0.7,
115+
});
116+
117+
// Custom embedding search
118+
const results = await client.vectorFieldSearch('articles', {
119+
field: 'content_embedding',
120+
query_vector: [0.012, -0.034, 0.056 /* ... */],
121+
top_k: 20,
122+
});
123+
124+
// Hybrid text + vector search
125+
const results = await client.hybridSearch('articles', {
126+
query: 'ML applications',
127+
find_text: { query: 'machine learning' },
128+
vector_weight: 0.7,
129+
text_weight: 0.3,
130+
});
131+
132+
// Boosted search (keywords boosted by vector similarity)
133+
const results = await client.boostedSearch('articles', {
134+
find_text: { query: 'python tutorial' },
135+
query: 'beginner programming guide',
136+
boost_factor: 1.5,
137+
});
138+
139+
// Extra parameters (where, sort) are forwarded to the API
140+
const results = await client.vectorSearch('articles', {
141+
query: 'climate change',
142+
limit: 5,
143+
sort: '-published_at',
144+
where: { category: 'science' },
145+
});
146+
```
147+
148+
You can also use `buildSearchBody()` for full control with the raw `search()` method:
149+
150+
```typescript
151+
const body = buildSearchBody({
152+
search_mode: SearchMode.HYBRID,
153+
find_text: { query: 'python' },
154+
vector_search: { query: 'programming tutorials', top_k: 10 },
155+
hybrid_config: { vector_weight: 0.6, text_weight: 0.4 },
156+
limit: 20,
157+
});
158+
const results = await client.search('articles', body);
159+
```
160+
103161
### API Folder Route Descriptions
104162

105163
You can configure per-route descriptions when connecting a folder to an API.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@foxnose/sdk",
3-
"version": "0.2.3",
3+
"version": "0.3.0",
44
"description": "Official FoxNose SDK for TypeScript and JavaScript",
55
"license": "Apache-2.0",
66
"type": "module",

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const DEFAULT_RETRY_CONFIG: Readonly<RetryConfig> = {
1919
methods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'],
2020
};
2121

22-
export const SDK_VERSION = '0.2.1';
22+
export const SDK_VERSION = '0.3.0';
2323

2424
export const DEFAULT_USER_AGENT = `foxnose-sdk-js/${SDK_VERSION}`;
2525

src/flux/client.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import type { AuthStrategy } from '../auth/types.js';
22
import type { RetryConfig } from '../config.js';
33
import { createConfig } from '../config.js';
44
import { HttpTransport } from '../http.js';
5+
import type {
6+
HybridConfig,
7+
SearchRequest,
8+
VectorBoostConfig,
9+
VectorFieldSearch,
10+
VectorSearch,
11+
} from './models.js';
12+
import { SearchMode, buildSearchBody, mergeExtra } from './models.js';
513

614
function cleanPrefix(prefix: string): string {
715
const value = prefix.replace(/^\/+|\/+$/g, '');
@@ -24,6 +32,65 @@ export interface FluxClientOptions {
2432
defaultHeaders?: Record<string, string>;
2533
}
2634

35+
export interface VectorSearchOptions {
36+
query: string;
37+
fields?: string[];
38+
top_k?: number;
39+
similarity_threshold?: number;
40+
limit?: number;
41+
offset?: number;
42+
[extra: string]: any;
43+
}
44+
45+
export interface VectorFieldSearchOptions {
46+
field: string;
47+
query_vector: number[];
48+
top_k?: number;
49+
similarity_threshold?: number;
50+
limit?: number;
51+
offset?: number;
52+
[extra: string]: any;
53+
}
54+
55+
export interface HybridSearchOptions {
56+
query: string;
57+
find_text: Record<string, any>;
58+
fields?: string[];
59+
top_k?: number;
60+
similarity_threshold?: number;
61+
vector_weight?: number;
62+
text_weight?: number;
63+
rerank_results?: boolean;
64+
limit?: number;
65+
offset?: number;
66+
[extra: string]: any;
67+
}
68+
69+
export interface BoostedSearchOptions {
70+
find_text: Record<string, any>;
71+
query?: string;
72+
field?: string;
73+
query_vector?: number[];
74+
top_k?: number;
75+
similarity_threshold?: number;
76+
boost_factor?: number;
77+
boost_similarity_threshold?: number;
78+
max_boost_results?: number;
79+
limit?: number;
80+
offset?: number;
81+
[extra: string]: any;
82+
}
83+
84+
function stripUndefined(obj: Record<string, any>): Record<string, any> {
85+
const result: Record<string, any> = {};
86+
for (const [key, value] of Object.entries(obj)) {
87+
if (value !== undefined) {
88+
result[key] = value;
89+
}
90+
}
91+
return result;
92+
}
93+
2794
/**
2895
* Client for FoxNose Flux delivery APIs.
2996
*
@@ -72,6 +139,138 @@ export class FluxClient {
72139
return this.transport.request('POST', path, { jsonBody: body });
73140
}
74141

142+
/** Semantic search using auto-generated embeddings. */
143+
async vectorSearch<T = any>(folderPath: string, options: VectorSearchOptions): Promise<T> {
144+
const { query, fields, top_k = 10, similarity_threshold, limit, offset, ...rest } = options;
145+
const extra = stripUndefined(rest);
146+
const vs: VectorSearch = { query, fields, top_k, similarity_threshold };
147+
const req: SearchRequest = {
148+
search_mode: SearchMode.VECTOR,
149+
vector_search: vs,
150+
limit,
151+
offset,
152+
};
153+
const body = mergeExtra(buildSearchBody(req), extra);
154+
return this.search(folderPath, body);
155+
}
156+
157+
/** Search using custom pre-computed embeddings. */
158+
async vectorFieldSearch<T = any>(
159+
folderPath: string,
160+
options: VectorFieldSearchOptions,
161+
): Promise<T> {
162+
const {
163+
field,
164+
query_vector,
165+
top_k = 10,
166+
similarity_threshold,
167+
limit,
168+
offset,
169+
...rest
170+
} = options;
171+
const extra = stripUndefined(rest);
172+
const vfs: VectorFieldSearch = { field, query_vector, top_k, similarity_threshold };
173+
const req: SearchRequest = {
174+
search_mode: SearchMode.VECTOR,
175+
vector_field_search: vfs,
176+
limit,
177+
offset,
178+
};
179+
const body = mergeExtra(buildSearchBody(req), extra);
180+
return this.search(folderPath, body);
181+
}
182+
183+
/** Blended text + vector search with configurable weights. */
184+
async hybridSearch<T = any>(folderPath: string, options: HybridSearchOptions): Promise<T> {
185+
const {
186+
query,
187+
find_text,
188+
fields,
189+
top_k = 10,
190+
similarity_threshold,
191+
vector_weight = 0.6,
192+
text_weight = 0.4,
193+
rerank_results = true,
194+
limit,
195+
offset,
196+
...rest
197+
} = options;
198+
const extra = stripUndefined(rest);
199+
const vs: VectorSearch = { query, fields, top_k, similarity_threshold };
200+
const hybrid: HybridConfig = { vector_weight, text_weight, rerank_results };
201+
const req: SearchRequest = {
202+
search_mode: SearchMode.HYBRID,
203+
find_text,
204+
vector_search: vs,
205+
hybrid_config: hybrid,
206+
limit,
207+
offset,
208+
};
209+
const body = mergeExtra(buildSearchBody(req), extra);
210+
return this.search(folderPath, body);
211+
}
212+
213+
/** Text search with results boosted by vector similarity. */
214+
async boostedSearch<T = any>(folderPath: string, options: BoostedSearchOptions): Promise<T> {
215+
const {
216+
find_text,
217+
query,
218+
field,
219+
query_vector,
220+
top_k = 10,
221+
similarity_threshold,
222+
boost_factor = 1.5,
223+
boost_similarity_threshold,
224+
max_boost_results = 20,
225+
limit,
226+
offset,
227+
...rest
228+
} = options;
229+
const extra = stripUndefined(rest);
230+
231+
const hasAuto = query != null;
232+
const hasCustom = field != null || query_vector != null;
233+
234+
if (hasAuto && hasCustom) {
235+
throw new Error(
236+
"Provide either 'query' for auto-generated embeddings " +
237+
"or 'field' + 'query_vector' for custom embeddings, not both",
238+
);
239+
}
240+
241+
let vs: VectorSearch | undefined;
242+
let vfs: VectorFieldSearch | undefined;
243+
244+
if (hasAuto) {
245+
vs = { query: query!, top_k, similarity_threshold };
246+
} else if (field != null && query_vector != null) {
247+
vfs = { field, query_vector, top_k, similarity_threshold };
248+
} else {
249+
throw new Error(
250+
"Provide either 'query' for auto-generated embeddings " +
251+
"or 'field' + 'query_vector' for custom embeddings",
252+
);
253+
}
254+
255+
const boostConfig: VectorBoostConfig = {
256+
boost_factor,
257+
similarity_threshold: boost_similarity_threshold,
258+
max_boost_results,
259+
};
260+
261+
const req: SearchRequest = {
262+
search_mode: SearchMode.VECTOR_BOOSTED,
263+
find_text,
264+
vector_search: vs,
265+
vector_field_search: vfs,
266+
vector_boost_config: boostConfig,
267+
limit,
268+
offset,
269+
};
270+
const body = mergeExtra(buildSearchBody(req), extra);
271+
return this.search(folderPath, body);
272+
}
273+
75274
async getRouter<T = any>(): Promise<T> {
76275
const path = `/${this.apiPrefix}/_router`;
77276
return this.transport.request('GET', path);

src/flux/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,16 @@
11
export { FluxClient } from './client.js';
2-
export type { FluxClientOptions } from './client.js';
2+
export type {
3+
FluxClientOptions,
4+
VectorSearchOptions,
5+
VectorFieldSearchOptions,
6+
HybridSearchOptions,
7+
BoostedSearchOptions,
8+
} from './client.js';
9+
export { SearchMode, buildSearchBody, mergeExtra } from './models.js';
10+
export type {
11+
VectorSearch,
12+
VectorFieldSearch,
13+
VectorBoostConfig,
14+
HybridConfig,
15+
SearchRequest,
16+
} from './models.js';

0 commit comments

Comments
 (0)