@volter/twin-turbopuffer 0.1.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/LICENSE +202 -0
- package/README.md +145 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +27 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +65 -0
- package/dist/src/key-gate.d.ts +3 -0
- package/dist/src/key-gate.js +39 -0
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +28 -0
- package/dist/src/screens/dashboard.d.ts +11 -0
- package/dist/src/screens/dashboard.js +191 -0
- package/dist/src/semantics/namespaces.d.ts +5 -0
- package/dist/src/semantics/namespaces.js +17 -0
- package/dist/src/turbopuffer-capabilities.d.ts +6 -0
- package/dist/src/turbopuffer-capabilities.js +442 -0
- package/dist/src/turbopuffer-conformance.d.ts +8 -0
- package/dist/src/turbopuffer-conformance.js +102 -0
- package/dist/src/turbopuffer-connector.d.ts +34 -0
- package/dist/src/turbopuffer-connector.js +152 -0
- package/dist/src/turbopuffer-filter.d.ts +57 -0
- package/dist/src/turbopuffer-filter.js +286 -0
- package/dist/src/turbopuffer-server.d.ts +23 -0
- package/dist/src/turbopuffer-server.js +72 -0
- package/dist/src/turbopuffer-stem.d.ts +1 -0
- package/dist/src/turbopuffer-stem.js +133 -0
- package/dist/src/turbopuffer-store.d.ts +80 -0
- package/dist/src/turbopuffer-store.js +1304 -0
- package/dist/src/turbopuffer-text.d.ts +44 -0
- package/dist/src/turbopuffer-text.js +189 -0
- package/dist/src/turbopuffer-twin.d.ts +25 -0
- package/dist/src/turbopuffer-twin.js +406 -0
- package/package.json +56 -0
- package/src/cli.ts +28 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/index.ts +92 -0
- package/src/key-gate.ts +39 -0
- package/src/manifest.ts +63 -0
- package/src/screens/dashboard.tsx +214 -0
- package/src/semantics/namespaces.ts +30 -0
- package/src/turbopuffer-capabilities.ts +455 -0
- package/src/turbopuffer-conformance.ts +101 -0
- package/src/turbopuffer-connector.ts +153 -0
- package/src/turbopuffer-filter.ts +277 -0
- package/src/turbopuffer-server.ts +81 -0
- package/src/turbopuffer-stem.ts +104 -0
- package/src/turbopuffer-store.ts +1157 -0
- package/src/turbopuffer-text.ts +204 -0
- package/src/turbopuffer-twin.ts +429 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// ENGLISH STEMMING — `full_text_search: {stemming: true}` with `language: "english"`: "Language-specific stemming for
|
|
2
|
+
// the text" (https://turbopuffer.com/docs/write, full_text_search). turbopuffer's analyzer (alyze, which "Includes a
|
|
3
|
+
// complete analyzer implementation, with support for lowercasing, ASCII case folding, stemming & stopword removal",
|
|
4
|
+
// github.com/turbopuffer/alyze src/analyze/mod.rs) stems with the `rust_stemmers` crate's Snowball algorithms; for
|
|
5
|
+
// English that is the Snowball English ("Porter2") stemmer, written out here from its published definition
|
|
6
|
+
// (https://snowballstem.org/algorithms/english/stemmer.html). Other languages' stemmers are not modelled.
|
|
7
|
+
const VOWELS = new Set(['a', 'e', 'i', 'o', 'u', 'y']);
|
|
8
|
+
const isVowel = (w, i) => VOWELS.has(w[i]);
|
|
9
|
+
const DOUBLES = ['bb', 'dd', 'ff', 'gg', 'mm', 'nn', 'pp', 'rr', 'tt'];
|
|
10
|
+
const LI_ENDINGS = new Set(['c', 'd', 'e', 'g', 'h', 'k', 'm', 'n', 'r', 't']);
|
|
11
|
+
const EXCEPTIONS = {
|
|
12
|
+
skis: 'ski', skies: 'sky', dying: 'die', lying: 'lie', tying: 'tie', idly: 'idl', gently: 'gentl', ugly: 'ugli',
|
|
13
|
+
early: 'earli', only: 'onli', singly: 'singl', sky: 'sky', news: 'news', howe: 'howe', atlas: 'atlas', cosmos: 'cosmos',
|
|
14
|
+
bias: 'bias', andes: 'andes',
|
|
15
|
+
};
|
|
16
|
+
const AFTER_1A = new Set(['inning', 'outing', 'canning', 'herring', 'earring', 'proceed', 'exceed', 'succeed']);
|
|
17
|
+
/** The start of the region after the first non-vowel following a vowel, from `from`. */
|
|
18
|
+
function regionAfter(w, from) {
|
|
19
|
+
for (let i = from + 1; i < w.length; i++)
|
|
20
|
+
if (!isVowel(w, i) && isVowel(w, i - 1))
|
|
21
|
+
return i + 1;
|
|
22
|
+
return w.length;
|
|
23
|
+
}
|
|
24
|
+
/** A short syllable ending at index `i` (inclusive). */
|
|
25
|
+
function shortSyllableAt(w, i) {
|
|
26
|
+
if (i === 1)
|
|
27
|
+
return isVowel(w, 0) && !isVowel(w, 1);
|
|
28
|
+
if (i < 2)
|
|
29
|
+
return false;
|
|
30
|
+
return !isVowel(w, i - 2) && isVowel(w, i - 1) && !isVowel(w, i) && !['w', 'x', 'Y'].includes(w[i]);
|
|
31
|
+
}
|
|
32
|
+
/** Step 2's rewrite of a suffix in R1: ogi to og after l, li deleted after a valid li-ending, the rest by the table. */
|
|
33
|
+
function step2(w, s2, table) {
|
|
34
|
+
if (s2 === 'ogi')
|
|
35
|
+
return w[w.length - 4] === 'l' ? `${w.slice(0, -3)}og` : w;
|
|
36
|
+
if (s2 === 'li')
|
|
37
|
+
return LI_ENDINGS.has(w[w.length - 3] ?? '') ? w.slice(0, -2) : w;
|
|
38
|
+
return w.slice(0, -s2.length) + table[s2];
|
|
39
|
+
}
|
|
40
|
+
/** Step 3's rewrite of a suffix in R1 by the table (ative, which needs R2, is the caller's). */
|
|
41
|
+
function step3(w, s3, table) {
|
|
42
|
+
return w.slice(0, -s3.length) + table[s3];
|
|
43
|
+
}
|
|
44
|
+
export function stemEnglish(word) {
|
|
45
|
+
if (word.length <= 2)
|
|
46
|
+
return word;
|
|
47
|
+
if (EXCEPTIONS[word] !== undefined)
|
|
48
|
+
return EXCEPTIONS[word];
|
|
49
|
+
let w = word.startsWith("'") ? word.slice(1) : word;
|
|
50
|
+
// initial y, and y after a vowel, are consonants: marked Y
|
|
51
|
+
w = w.replace(/^y/, 'Y').replace(/([aeiouy])y/g, '$1Y');
|
|
52
|
+
let r1 = /^(gener|commun|arsen)/.test(w) ? /^(gener|commun|arsen)/.exec(w)[0].length : regionAfter(w, 0);
|
|
53
|
+
const r2 = () => regionAfter(w, r1);
|
|
54
|
+
const inR1 = (suffix) => w.length - suffix.length >= r1;
|
|
55
|
+
const inR2 = (suffix) => w.length - suffix.length >= r2();
|
|
56
|
+
const ends = (s) => w.endsWith(s);
|
|
57
|
+
const cut = (n) => { w = w.slice(0, w.length - n); };
|
|
58
|
+
const longest = (list) => list.filter(ends).sort((a, b) => b.length - a.length)[0];
|
|
59
|
+
const hasVowelBefore = (end) => { for (let i = 0; i < end; i++)
|
|
60
|
+
if (isVowel(w, i))
|
|
61
|
+
return true; return false; };
|
|
62
|
+
// Step 0
|
|
63
|
+
const s0 = longest(["'s'", "'s", "'"]);
|
|
64
|
+
if (s0)
|
|
65
|
+
cut(s0.length);
|
|
66
|
+
// Step 1a
|
|
67
|
+
const s1a = longest(['sses', 'ied', 'ies', 'us', 'ss', 's']);
|
|
68
|
+
if (s1a === 'sses')
|
|
69
|
+
cut(2);
|
|
70
|
+
else if (s1a === 'ied' || s1a === 'ies') {
|
|
71
|
+
cut(3);
|
|
72
|
+
w += w.length > 1 ? 'i' : 'ie';
|
|
73
|
+
}
|
|
74
|
+
else if (s1a === 's') {
|
|
75
|
+
if (hasVowelBefore(w.length - 2))
|
|
76
|
+
cut(1);
|
|
77
|
+
}
|
|
78
|
+
if (AFTER_1A.has(w))
|
|
79
|
+
return w;
|
|
80
|
+
// Step 1b
|
|
81
|
+
const s1b = longest(['eedly', 'eed', 'ingly', 'edly', 'ing', 'ed']);
|
|
82
|
+
if (s1b === 'eed' || s1b === 'eedly') {
|
|
83
|
+
if (inR1(s1b)) {
|
|
84
|
+
cut(s1b.length);
|
|
85
|
+
w += 'ee';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (s1b && hasVowelBefore(w.length - s1b.length)) {
|
|
89
|
+
cut(s1b.length);
|
|
90
|
+
if (ends('at') || ends('bl') || ends('iz'))
|
|
91
|
+
w += 'e';
|
|
92
|
+
else if (DOUBLES.some(ends))
|
|
93
|
+
cut(1);
|
|
94
|
+
else if (r1 >= w.length && shortSyllableAt(w, w.length - 1))
|
|
95
|
+
w += 'e';
|
|
96
|
+
}
|
|
97
|
+
// Step 1c
|
|
98
|
+
if ((ends('y') || ends('Y')) && w.length > 2 && !isVowel(w, w.length - 2))
|
|
99
|
+
w = `${w.slice(0, -1)}i`;
|
|
100
|
+
// Step 2
|
|
101
|
+
const STEP2 = {
|
|
102
|
+
tional: 'tion', enci: 'ence', anci: 'ance', abli: 'able', entli: 'ent', izer: 'ize', ization: 'ize', ational: 'ate',
|
|
103
|
+
ation: 'ate', ator: 'ate', alism: 'al', aliti: 'al', alli: 'al', fulness: 'ful', ousli: 'ous', ousness: 'ous',
|
|
104
|
+
iveness: 'ive', iviti: 'ive', biliti: 'ble', bli: 'ble', ogi: 'og', fulli: 'ful', lessli: 'less', li: '',
|
|
105
|
+
};
|
|
106
|
+
const s2 = longest(Object.keys(STEP2));
|
|
107
|
+
if (s2 && inR1(s2))
|
|
108
|
+
w = step2(w, s2, STEP2);
|
|
109
|
+
// Step 3
|
|
110
|
+
const STEP3 = { tional: 'tion', ational: 'ate', alize: 'al', icate: 'ic', iciti: 'ic', ical: 'ic', ful: '', ness: '', ative: '' };
|
|
111
|
+
const s3 = longest(Object.keys(STEP3));
|
|
112
|
+
if (s3 && inR1(s3))
|
|
113
|
+
w = s3 === 'ative' ? (inR2(s3) ? w.slice(0, -5) : w) : step3(w, s3, STEP3);
|
|
114
|
+
// Step 4
|
|
115
|
+
const s4 = longest(['al', 'ance', 'ence', 'er', 'ic', 'able', 'ible', 'ant', 'ement', 'ment', 'ent', 'ism', 'ate', 'iti', 'ous', 'ive', 'ize', 'ion']);
|
|
116
|
+
if (s4 && inR2(s4)) {
|
|
117
|
+
if (s4 === 'ion') {
|
|
118
|
+
if (['s', 't'].includes(w[w.length - 4] ?? ''))
|
|
119
|
+
cut(3);
|
|
120
|
+
}
|
|
121
|
+
else
|
|
122
|
+
cut(s4.length);
|
|
123
|
+
}
|
|
124
|
+
// Step 5
|
|
125
|
+
if (ends('e')) {
|
|
126
|
+
if (inR2('e') || (inR1('e') && !shortSyllableAt(w, w.length - 2)))
|
|
127
|
+
cut(1);
|
|
128
|
+
}
|
|
129
|
+
else if (ends('l') && inR2('l') && w[w.length - 2] === 'l')
|
|
130
|
+
cut(1);
|
|
131
|
+
r1 = 0;
|
|
132
|
+
return w.replace(/Y/g, 'y');
|
|
133
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type TwinResource } from '@volter/world-core';
|
|
2
|
+
import { TurbopufferError, type AttrConfig, type Doc } from './turbopuffer-filter.js';
|
|
3
|
+
export { badRequest, TurbopufferError } from './turbopuffer-filter.js';
|
|
4
|
+
export type { AttrConfig, Doc } from './turbopuffer-filter.js';
|
|
5
|
+
export declare const SERVICE = "turbopuffer";
|
|
6
|
+
export declare const TURBOPUFFER_RESOURCE_TYPES: readonly ["namespace", "document"];
|
|
7
|
+
export declare const DISTANCE_METRICS: readonly ["cosine_distance", "euclidean_squared"];
|
|
8
|
+
export type NamespaceState = {
|
|
9
|
+
name: string;
|
|
10
|
+
schema: Record<string, AttrConfig>;
|
|
11
|
+
distance_metric: string | null;
|
|
12
|
+
vector_dims: number | null;
|
|
13
|
+
created_at: string;
|
|
14
|
+
updated_at: string;
|
|
15
|
+
/** when its documents last changed (a schema-only write moves `updated_at` alone) */
|
|
16
|
+
last_write_at: string;
|
|
17
|
+
/** the namespace a branch was made from (metadata's `branching.parent`) */
|
|
18
|
+
parent?: string;
|
|
19
|
+
docs: Map<string, Doc>;
|
|
20
|
+
};
|
|
21
|
+
export declare function assertNamespaceName(name: string): void;
|
|
22
|
+
export declare function docKey(id: string | number): string;
|
|
23
|
+
export declare function documentSubjectId(ns: string, id: string | number): string;
|
|
24
|
+
/** Fold the tree's `namespace` / `document` subjects into live namespaces. */
|
|
25
|
+
export declare function loadNamespaces(resources: readonly TwinResource[]): Map<string, NamespaceState>;
|
|
26
|
+
export declare function namespaceFields(ns: NamespaceState): Record<string, unknown>;
|
|
27
|
+
export declare function documentFields(ns: string, d: Doc): Record<string, unknown>;
|
|
28
|
+
/** "Vectors are attributes with a vector type (`[N]f32`, `[N]f16`, or `[N]i8` where N is the number of dimensions)"
|
|
29
|
+
* (https://turbopuffer.com/docs/write, Vectors). */
|
|
30
|
+
export declare function isVectorType(t: string): boolean;
|
|
31
|
+
/** "`[][N]f32`: Variable-length array of `N` dimensional f32 vectors, used for late-interaction (multi-vector) search"
|
|
32
|
+
* (https://turbopuffer.com/docs/write, type). */
|
|
33
|
+
export declare function isVectorArrayType(t: string): boolean;
|
|
34
|
+
/** Normalize one schema entry (`"string"` or a config object) into the stored form. */
|
|
35
|
+
export declare function normalizeAttrSchema(attr: string, raw: unknown): AttrConfig;
|
|
36
|
+
export type WriteOutcome = {
|
|
37
|
+
next: NamespaceState;
|
|
38
|
+
created: boolean;
|
|
39
|
+
upserts: Doc[];
|
|
40
|
+
deletes: string[];
|
|
41
|
+
response: Record<string, unknown>;
|
|
42
|
+
};
|
|
43
|
+
export declare function approxLogicalBytes(ns: NamespaceState): number;
|
|
44
|
+
/** `copy_from_namespace` and `branch_from_namespace` (https://turbopuffer.com/docs/write): the destination is made as a
|
|
45
|
+
* copy of the source's documents, schema and distance metric. "The destination namespace you are copying into must be
|
|
46
|
+
* empty. The initial request currently cannot make schema changes or contain documents"; a branch is "an instant
|
|
47
|
+
* copy-on-write clone of the source namespace. The destination namespace must be empty", and "reads, writes, queries,
|
|
48
|
+
* and deletes on one namespace do not affect the other". A copy answers "namespace cloned successfully" (the async
|
|
49
|
+
* copy's result, https://turbopuffer.com/docs/api-overview), and a destination that exists "destination namespace
|
|
50
|
+
* already exists" (the same page's failed result).
|
|
51
|
+
*
|
|
52
|
+
* Where the docs stop and the twin decides: a World holds one organization's namespaces in one store whatever region a
|
|
53
|
+
* request names, so a copy's `source_region` and `source_api_key` read the same store; `rows_affected` counts the
|
|
54
|
+
* documents cloned; a copy bills the logical bytes copied and a branch none ("billed at a flat rate"); a branch's
|
|
55
|
+
* answer carries the copy's message. Pure. */
|
|
56
|
+
export declare function applyClone(spaces: Map<string, NamespaceState>, name: string, rawBody: unknown, at: string): WriteOutcome;
|
|
57
|
+
/** Apply one write request body to a namespace (or create it). Pure. */
|
|
58
|
+
export declare function applyWrite(existing: NamespaceState | undefined, name: string, rawBody: unknown, at: string): WriteOutcome;
|
|
59
|
+
export declare const EMBEDDING_MODELS: string[];
|
|
60
|
+
/** The placeholder embedding of a text: a unit vector of `dims` derived from the model's name and the text. */
|
|
61
|
+
export declare function placeholderEmbedding(model: string, text: string, dims: number): number[];
|
|
62
|
+
export type QueryResult = {
|
|
63
|
+
rows?: Record<string, unknown>[];
|
|
64
|
+
aggregations?: Record<string, unknown>;
|
|
65
|
+
aggregation_groups?: Record<string, unknown>[];
|
|
66
|
+
};
|
|
67
|
+
/** `vector_encoding` other than float: "base64" is documented ("The encoding to use for vectors in the response", the
|
|
68
|
+
* spec's VectorEncoding) and not modelled; anything else is refused. */
|
|
69
|
+
export declare function vectorEncodingRefusal(v: unknown): TurbopufferError;
|
|
70
|
+
/** Answer one query (the body of `POST /v2/namespaces/{ns}/query`, or one of `queries[]`). Pure. */
|
|
71
|
+
export declare function runQuery(ns: NamespaceState, rawQuery: unknown, opts?: {
|
|
72
|
+
multi?: boolean;
|
|
73
|
+
}): QueryResult;
|
|
74
|
+
export declare function checkConsistency(c: unknown): void;
|
|
75
|
+
export declare function cosineDistance(a: readonly number[], b: readonly number[]): number;
|
|
76
|
+
export declare function euclideanSquared(a: readonly number[], b: readonly number[]): number;
|
|
77
|
+
/** The wire form of a namespace's schema: every attribute with its full resolved config. */
|
|
78
|
+
export declare function schemaWire(ns: NamespaceState): Record<string, unknown>;
|
|
79
|
+
export declare function queryBilling(ns: NamespaceState, returned: unknown): Record<string, number>;
|
|
80
|
+
export declare function queryPerformance(ns: NamespaceState): Record<string, unknown>;
|