schift-cli 0.1.0__py3-none-any.whl
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.
- schift_cli/__init__.py +1 -0
- schift_cli/client.py +119 -0
- schift_cli/commands/__init__.py +0 -0
- schift_cli/commands/auth.py +68 -0
- schift_cli/commands/bench.py +65 -0
- schift_cli/commands/catalog.py +74 -0
- schift_cli/commands/db.py +96 -0
- schift_cli/commands/embed.py +104 -0
- schift_cli/commands/migrate.py +127 -0
- schift_cli/commands/query.py +66 -0
- schift_cli/commands/skill.py +110 -0
- schift_cli/commands/usage.py +50 -0
- schift_cli/config.py +58 -0
- schift_cli/data/schift-best-practices/AGENTS.md +77 -0
- schift_cli/data/schift-best-practices/CLAUDE.md +77 -0
- schift_cli/data/schift-best-practices/SKILL.md +89 -0
- schift_cli/data/schift-best-practices/references/bucket-organization.md +126 -0
- schift_cli/data/schift-best-practices/references/bucket-upload.md +116 -0
- schift_cli/data/schift-best-practices/references/chatbot-widget.md +238 -0
- schift_cli/data/schift-best-practices/references/cost-batching.md +179 -0
- schift_cli/data/schift-best-practices/references/cost-storage-tiers.md +183 -0
- schift_cli/data/schift-best-practices/references/deploy-cloudrun.md +140 -0
- schift_cli/data/schift-best-practices/references/embed-batch-processing.md +86 -0
- schift_cli/data/schift-best-practices/references/embed-error-handling.md +155 -0
- schift_cli/data/schift-best-practices/references/embed-multimodal.md +100 -0
- schift_cli/data/schift-best-practices/references/embed-task-types.md +135 -0
- schift_cli/data/schift-best-practices/references/rag-chunking.md +173 -0
- schift_cli/data/schift-best-practices/references/rag-workflow-builder.md +205 -0
- schift_cli/data/schift-best-practices/references/sdk-async-patterns.md +103 -0
- schift_cli/data/schift-best-practices/references/sdk-auth-patterns.md +76 -0
- schift_cli/data/schift-best-practices/references/search-collection-design.md +229 -0
- schift_cli/data/schift-best-practices/references/search-hybrid.md +163 -0
- schift_cli/data/schift-best-practices/references/search-similarity-tuning.md +134 -0
- schift_cli/display.py +85 -0
- schift_cli/main.py +39 -0
- schift_cli-0.1.0.dist-info/METADATA +12 -0
- schift_cli-0.1.0.dist-info/RECORD +40 -0
- schift_cli-0.1.0.dist-info/WHEEL +5 -0
- schift_cli-0.1.0.dist-info/entry_points.txt +2 -0
- schift_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Bucket Upload for Document Ingestion
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Manual chunking and embedding pipelines are error-prone and skip OCR, layout parsing, and deduplication that the bucket pipeline performs automatically. Using bucket.upload() reduces ingestion code from ~50 lines to 2.
|
|
5
|
+
tags: [bucket, upload, ingestion, ocr, chunking, pdf, docx]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use bucket.upload() for Document Ingestion
|
|
9
|
+
|
|
10
|
+
Schift buckets provide a fully managed ingestion pipeline: upload raw files and the bucket handles OCR, chunking, embedding, and FAISS index updates automatically. Building this pipeline manually duplicates work the platform already does and introduces subtle inconsistencies (chunk overlap, encoding issues, missed page breaks).
|
|
11
|
+
|
|
12
|
+
Supported formats: **PDF, DOCX, TXT, MD**, and images (PNG, JPG, WEBP) with automatic OCR.
|
|
13
|
+
|
|
14
|
+
### Incorrect
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# Python - manual pipeline: reads file, chunks, embeds, adds to collection
|
|
18
|
+
import os
|
|
19
|
+
from schift import Schift
|
|
20
|
+
|
|
21
|
+
client = Schift()
|
|
22
|
+
|
|
23
|
+
def ingest_pdf(file_path: str, collection_id: str):
|
|
24
|
+
# Step 1: extract text manually (no layout awareness, no OCR for scanned pages)
|
|
25
|
+
import PyPDF2
|
|
26
|
+
with open(file_path, "rb") as f:
|
|
27
|
+
reader = PyPDF2.PdfReader(f)
|
|
28
|
+
full_text = " ".join(page.extract_text() for page in reader.pages)
|
|
29
|
+
|
|
30
|
+
# Step 2: chunk manually (naive split, loses sentence boundaries)
|
|
31
|
+
chunk_size = 500
|
|
32
|
+
chunks = [full_text[i:i+chunk_size] for i in range(0, len(full_text), chunk_size)]
|
|
33
|
+
|
|
34
|
+
# Step 3: embed each chunk (no deduplication, no credit hold)
|
|
35
|
+
embeddings = client.embed_batch(chunks)
|
|
36
|
+
|
|
37
|
+
# Step 4: add to collection (separate API call per chunk batch)
|
|
38
|
+
for chunk, embedding in zip(chunks, embeddings.vectors):
|
|
39
|
+
client.collection.add(collection_id, text=chunk, vector=embedding)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// TypeScript - same manual approach
|
|
44
|
+
import { Schift } from '@schift-io/sdk';
|
|
45
|
+
import * as fs from 'fs';
|
|
46
|
+
|
|
47
|
+
const client = new Schift();
|
|
48
|
+
|
|
49
|
+
async function ingestPdf(filePath: string, collectionId: string) {
|
|
50
|
+
// manual extraction, chunking, embedding, and indexing
|
|
51
|
+
const text = extractTextFromPdf(filePath); // not shown, error-prone
|
|
52
|
+
const chunks = splitIntoChunks(text, 500); // naive chunker
|
|
53
|
+
const embeddings = await client.embedBatch(chunks);
|
|
54
|
+
for (const [chunk, vec] of zip(chunks, embeddings.vectors)) {
|
|
55
|
+
await client.collection.add(collectionId, { text: chunk, vector: vec });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This approach misses scanned-page OCR, skips layout heuristics (headers, tables, footnotes), does no deduplication on re-upload, and requires maintaining the chunking logic yourself.
|
|
61
|
+
|
|
62
|
+
### Correct
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# Python - bucket.upload() handles the entire pipeline
|
|
66
|
+
import os
|
|
67
|
+
from schift import Schift
|
|
68
|
+
|
|
69
|
+
client = Schift()
|
|
70
|
+
|
|
71
|
+
# Create the bucket once (idempotent name)
|
|
72
|
+
bucket = client.bucket.create("legal-contracts")
|
|
73
|
+
|
|
74
|
+
# Upload one or many files; returns immediately with a job ID
|
|
75
|
+
job = client.bucket.upload(
|
|
76
|
+
bucket.id,
|
|
77
|
+
files=["contracts/acme_2026.pdf", "contracts/globex_2026.pdf"]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Optional: poll until ingestion completes before searching
|
|
81
|
+
job.wait() # blocks until all files are indexed
|
|
82
|
+
|
|
83
|
+
# Search across all uploaded documents
|
|
84
|
+
results = client.bucket.search(bucket.id, "indemnification clause")
|
|
85
|
+
for r in results.hits:
|
|
86
|
+
print(r.text, r.score)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// TypeScript - same two-step pattern
|
|
91
|
+
import { Schift } from '@schift-io/sdk';
|
|
92
|
+
import * as fs from 'fs';
|
|
93
|
+
|
|
94
|
+
const client = new Schift();
|
|
95
|
+
|
|
96
|
+
const bucket = await client.bucket.create('legal-contracts');
|
|
97
|
+
|
|
98
|
+
const job = await client.bucket.upload(bucket.id, {
|
|
99
|
+
files: ['contracts/acme_2026.pdf', 'contracts/globex_2026.pdf'],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await job.wait(); // optional: wait for indexing to complete
|
|
103
|
+
|
|
104
|
+
const results = await client.bucket.search(bucket.id, 'indemnification clause');
|
|
105
|
+
results.hits.forEach(r => console.log(r.text, r.score));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Credit hold pattern**: `bucket.upload()` pre-deducts an estimated token cost at upload time (a hold), then refunds the difference once actual usage is measured. You are never charged more than the hold amount. This prevents quota overruns during large uploads.
|
|
109
|
+
|
|
110
|
+
**Re-upload deduplication**: Uploading the same file content twice to the same bucket is a no-op — Schift detects the content hash and skips re-embedding.
|
|
111
|
+
|
|
112
|
+
## Reference
|
|
113
|
+
|
|
114
|
+
- https://docs.schift.io/buckets/upload
|
|
115
|
+
- https://docs.schift.io/buckets/supported-formats
|
|
116
|
+
- https://docs.schift.io/billing/credit-hold
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Build a Chatbot with Schift Search and Conversation Memory
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: Production-ready chatbot with proper conversation context
|
|
5
|
+
tags:
|
|
6
|
+
- chatbot
|
|
7
|
+
- rag
|
|
8
|
+
- conversation
|
|
9
|
+
- memory
|
|
10
|
+
- search
|
|
11
|
+
- llm
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Build a Chatbot with Schift Search and Conversation Memory
|
|
15
|
+
|
|
16
|
+
A chatbot backed by Schift needs two things working together: vector search that retrieves relevant context from your data, and conversation history that lets the LLM give coherent follow-up answers. The most common mistake is only sending the latest user message — the LLM has no context for follow-ups like "tell me more" or "what about the second point?".
|
|
17
|
+
|
|
18
|
+
Use a retrieve-then-generate pattern:
|
|
19
|
+
1. User sends a message
|
|
20
|
+
2. Backend searches a Schift collection using the user's query
|
|
21
|
+
3. Retrieved chunks + conversation history are assembled into an LLM prompt
|
|
22
|
+
4. LLM generates a response via Schift's OpenAI-compatible proxy at `/v1/llm/chat/completions`
|
|
23
|
+
5. The conversation turn is stored in session
|
|
24
|
+
|
|
25
|
+
### Incorrect
|
|
26
|
+
|
|
27
|
+
Sending only the latest message to search and ignoring conversation history:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// TypeScript — loses context on every turn
|
|
31
|
+
app.post('/api/chat', async (c) => {
|
|
32
|
+
const { message } = await c.req.json();
|
|
33
|
+
|
|
34
|
+
// Only uses the current message — no history
|
|
35
|
+
const results = await client.search('col_...', message);
|
|
36
|
+
const context = results.map(r => r.text).join('\n');
|
|
37
|
+
|
|
38
|
+
// LLM has no idea what was said before
|
|
39
|
+
const response = await fetch('https://api.schift.io/v1/llm/chat/completions', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { Authorization: `Bearer ${process.env.SCHIFT_API_KEY}` },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
model: 'gpt-4o-mini',
|
|
44
|
+
messages: [
|
|
45
|
+
{ role: 'system', content: `Answer using this context:\n${context}` },
|
|
46
|
+
{ role: 'user', content: message }, // only the latest message
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
// ...
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# Python — same problem: no history, no context carryover
|
|
56
|
+
@app.post("/api/chat")
|
|
57
|
+
async def chat(body: ChatRequest):
|
|
58
|
+
results = client.search("col_...", body.message) # no history used
|
|
59
|
+
context = "\n".join(r.text for r in results)
|
|
60
|
+
# Stateless: every call is a fresh conversation
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Without history, the LLM cannot answer follow-ups, cannot refer back to earlier responses, and produces answers that feel disconnected and robotic.
|
|
64
|
+
|
|
65
|
+
### Correct
|
|
66
|
+
|
|
67
|
+
Maintain session-based conversation history, use the last 3–5 messages for search context, and pass full history to the LLM:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// server.ts — Hono backend with session memory and Schift RAG
|
|
71
|
+
import { Hono } from 'hono';
|
|
72
|
+
import { Schift } from '@schift-io/sdk';
|
|
73
|
+
|
|
74
|
+
const app = new Hono();
|
|
75
|
+
const client = new Schift({ apiKey: process.env.SCHIFT_API_KEY! });
|
|
76
|
+
|
|
77
|
+
// In-memory session store (swap for Redis in production)
|
|
78
|
+
const sessions = new Map<string, Array<{ role: string; content: string }>>();
|
|
79
|
+
|
|
80
|
+
const COLLECTION_ID = 'col_your_collection';
|
|
81
|
+
const SCHIFT_LLM_URL = 'https://api.schift.io/v1/llm/chat/completions';
|
|
82
|
+
|
|
83
|
+
app.post('/api/chat', async (c) => {
|
|
84
|
+
const { message, sessionId } = await c.req.json<{
|
|
85
|
+
message: string;
|
|
86
|
+
sessionId: string;
|
|
87
|
+
}>();
|
|
88
|
+
|
|
89
|
+
// 1. Load or initialize conversation history for this session
|
|
90
|
+
const history = sessions.get(sessionId) ?? [];
|
|
91
|
+
|
|
92
|
+
// 2. Build search query from current message + last 2 turns for context
|
|
93
|
+
// This helps disambiguate follow-ups like "tell me more about that"
|
|
94
|
+
const recentContext = history
|
|
95
|
+
.slice(-4) // last 2 user+assistant pairs
|
|
96
|
+
.map(m => m.content)
|
|
97
|
+
.join(' ');
|
|
98
|
+
const searchQuery = `${recentContext} ${message}`.trim();
|
|
99
|
+
|
|
100
|
+
// 3. Retrieve relevant chunks from Schift
|
|
101
|
+
const results = await client.search(COLLECTION_ID, searchQuery, {
|
|
102
|
+
topK: 4,
|
|
103
|
+
scoreThreshold: 0.70,
|
|
104
|
+
});
|
|
105
|
+
const retrievedContext = results.map(r => r.text).join('\n\n');
|
|
106
|
+
|
|
107
|
+
// 4. Assemble messages: system prompt + history + new user message
|
|
108
|
+
// Keep last 10 turns to stay within token limits
|
|
109
|
+
const trimmedHistory = history.slice(-10);
|
|
110
|
+
const messages = [
|
|
111
|
+
{
|
|
112
|
+
role: 'system',
|
|
113
|
+
content: [
|
|
114
|
+
'You are a helpful assistant. Answer based on the provided context.',
|
|
115
|
+
'If the context does not contain the answer, say so honestly.',
|
|
116
|
+
'',
|
|
117
|
+
'## Context',
|
|
118
|
+
retrievedContext || '(no relevant context found)',
|
|
119
|
+
].join('\n'),
|
|
120
|
+
},
|
|
121
|
+
...trimmedHistory,
|
|
122
|
+
{ role: 'user', content: message },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// 5. Call LLM via Schift's OpenAI-compatible proxy
|
|
126
|
+
// /v1/llm/chat/completions supports the same format as OpenAI
|
|
127
|
+
const llmResponse = await fetch(SCHIFT_LLM_URL, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
Authorization: `Bearer ${process.env.SCHIFT_API_KEY}`,
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
model: 'gpt-4o-mini',
|
|
135
|
+
messages,
|
|
136
|
+
temperature: 0.3, // lower temp for factual Q&A
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const llmData = await llmResponse.json<{
|
|
141
|
+
choices: Array<{ message: { content: string } }>;
|
|
142
|
+
}>();
|
|
143
|
+
const assistantMessage = llmData.choices[0].message.content;
|
|
144
|
+
|
|
145
|
+
// 6. Store this turn in session history
|
|
146
|
+
history.push({ role: 'user', content: message });
|
|
147
|
+
history.push({ role: 'assistant', content: assistantMessage });
|
|
148
|
+
sessions.set(sessionId, history);
|
|
149
|
+
|
|
150
|
+
return c.json({
|
|
151
|
+
response: assistantMessage,
|
|
152
|
+
sources: results.map(r => ({ id: r.id, score: r.score })),
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Clear session on explicit reset
|
|
157
|
+
app.delete('/api/chat/:sessionId', (c) => {
|
|
158
|
+
sessions.delete(c.req.param('sessionId'));
|
|
159
|
+
return c.json({ cleared: true });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export default app;
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Python (FastAPI) alternative:**
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# main.py — FastAPI equivalent
|
|
169
|
+
from fastapi import FastAPI
|
|
170
|
+
from pydantic import BaseModel
|
|
171
|
+
from schift import Schift
|
|
172
|
+
import httpx, os
|
|
173
|
+
|
|
174
|
+
app = FastAPI()
|
|
175
|
+
client = Schift(api_key=os.environ["SCHIFT_API_KEY"])
|
|
176
|
+
|
|
177
|
+
# In-memory store; swap for Redis in production
|
|
178
|
+
sessions: dict[str, list[dict]] = {}
|
|
179
|
+
|
|
180
|
+
COLLECTION_ID = "col_your_collection"
|
|
181
|
+
SCHIFT_LLM_URL = "https://api.schift.io/v1/llm/chat/completions"
|
|
182
|
+
|
|
183
|
+
class ChatRequest(BaseModel):
|
|
184
|
+
message: str
|
|
185
|
+
session_id: str
|
|
186
|
+
|
|
187
|
+
@app.post("/api/chat")
|
|
188
|
+
async def chat(body: ChatRequest):
|
|
189
|
+
history = sessions.setdefault(body.session_id, [])
|
|
190
|
+
|
|
191
|
+
# Build search query from recent conversation context
|
|
192
|
+
recent = " ".join(m["content"] for m in history[-4:])
|
|
193
|
+
search_query = f"{recent} {body.message}".strip()
|
|
194
|
+
|
|
195
|
+
# Retrieve from Schift
|
|
196
|
+
results = client.search(
|
|
197
|
+
COLLECTION_ID, search_query, top_k=4, score_threshold=0.70
|
|
198
|
+
)
|
|
199
|
+
context = "\n\n".join(r.text for r in results)
|
|
200
|
+
|
|
201
|
+
# Assemble messages
|
|
202
|
+
messages = [
|
|
203
|
+
{
|
|
204
|
+
"role": "system",
|
|
205
|
+
"content": f"Answer based on context. Be honest if unsure.\n\n## Context\n{context or '(none)'}",
|
|
206
|
+
},
|
|
207
|
+
*history[-10:],
|
|
208
|
+
{"role": "user", "content": body.message},
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
# Call Schift's LLM proxy (OpenAI-compatible)
|
|
212
|
+
async with httpx.AsyncClient() as http:
|
|
213
|
+
resp = await http.post(
|
|
214
|
+
SCHIFT_LLM_URL,
|
|
215
|
+
headers={"Authorization": f"Bearer {os.environ['SCHIFT_API_KEY']}"},
|
|
216
|
+
json={"model": "gpt-4o-mini", "messages": messages, "temperature": 0.3},
|
|
217
|
+
)
|
|
218
|
+
data = resp.json()
|
|
219
|
+
answer = data["choices"][0]["message"]["content"]
|
|
220
|
+
|
|
221
|
+
# Save turn
|
|
222
|
+
history.append({"role": "user", "content": body.message})
|
|
223
|
+
history.append({"role": "assistant", "content": answer})
|
|
224
|
+
|
|
225
|
+
return {"response": answer, "sources": [{"id": r.id, "score": r.score} for r in results]}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Production considerations:**
|
|
229
|
+
|
|
230
|
+
- Replace the in-memory `sessions` map with Redis (`ioredis` / `redis-py`) keyed by session ID with a TTL (e.g. 30 minutes)
|
|
231
|
+
- Cap `trimmedHistory` at 10 turns to keep the prompt within model context limits
|
|
232
|
+
- Use a lower `temperature` (0.1–0.4) for factual chatbots; higher (0.6–0.8) for creative assistants
|
|
233
|
+
- Schift's `/v1/llm/chat/completions` proxy accepts the same request format as OpenAI — you can swap models by changing the `model` field without changing any other code
|
|
234
|
+
|
|
235
|
+
## Reference
|
|
236
|
+
|
|
237
|
+
- https://docs.schift.io/llm/chat-completions
|
|
238
|
+
- https://docs.schift.io/search/overview
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Batch API Calls to Minimize Per-Call Overhead and Cost
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Each individual API call is billed as 1 unit regardless of how many texts it contains (up to 100). Sending 1000 single-text calls costs 100x more than sending 10 batches of 100 texts. Batching is the single highest-leverage cost optimization.
|
|
5
|
+
tags:
|
|
6
|
+
- cost
|
|
7
|
+
- batching
|
|
8
|
+
- embed
|
|
9
|
+
- performance
|
|
10
|
+
- throughput
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Batch API Calls to Minimize Per-Call Overhead and Cost
|
|
14
|
+
|
|
15
|
+
Schift billing is per API call, not per text. One call can embed up to 100 texts — and it costs exactly the same as one call embedding a single text. This means that how you structure your calls matters far more than how much text you send.
|
|
16
|
+
|
|
17
|
+
The rule is simple: never make one API call per document. Always accumulate texts into batches of up to 100 and call `embed_batch` / `embedBatch` once per batch.
|
|
18
|
+
|
|
19
|
+
**Billing unit**: 1 API call = 1 billing unit = $0.10 per 1,000 calls, regardless of batch size (up to 100 items per call).
|
|
20
|
+
|
|
21
|
+
### Incorrect
|
|
22
|
+
|
|
23
|
+
One embed call per text — 1000 texts = 1000 API calls = $0.10:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# Python — one call per document: 1000 texts = 1000 API calls = $0.10
|
|
27
|
+
from schift import Schift
|
|
28
|
+
|
|
29
|
+
client = Schift(api_key="sch_...")
|
|
30
|
+
|
|
31
|
+
documents = load_documents() # 1000 documents
|
|
32
|
+
|
|
33
|
+
# Every call is 1 billing unit — this is 1000 billing units
|
|
34
|
+
for doc in documents:
|
|
35
|
+
embedding = client.embed(doc.text) # 1 call per doc
|
|
36
|
+
store_embedding(doc.id, embedding)
|
|
37
|
+
|
|
38
|
+
# Cost: 1000 calls × ($0.10 / 1000 calls) = $0.10
|
|
39
|
+
# Latency: 1000 sequential round trips (or N parallel, but N connections)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// TypeScript — same problem: one call per document
|
|
44
|
+
import { Schift } from '@schift-io/sdk';
|
|
45
|
+
|
|
46
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
47
|
+
|
|
48
|
+
const documents = await loadDocuments(); // 1000 documents
|
|
49
|
+
|
|
50
|
+
// Sequential: slow and wasteful
|
|
51
|
+
for (const doc of documents) {
|
|
52
|
+
const embedding = await client.embed(doc.text); // 1 billing unit each
|
|
53
|
+
await storeEmbedding(doc.id, embedding);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Even with Promise.all, still 1000 separate API calls:
|
|
57
|
+
const embeddings = await Promise.all(
|
|
58
|
+
documents.map(doc => client.embed(doc.text)) // still 1000 calls
|
|
59
|
+
);
|
|
60
|
+
// Cost: 1000 billing units = $0.10
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Correct
|
|
64
|
+
|
|
65
|
+
Batch up to 100 texts per call — 1000 texts = 10 API calls = $0.001:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# Python — batch embedding: 1000 texts in 10 calls = 10 billing units = $0.001
|
|
69
|
+
from schift import Schift
|
|
70
|
+
|
|
71
|
+
client = Schift(api_key="sch_...")
|
|
72
|
+
|
|
73
|
+
documents = load_documents() # 1000 documents
|
|
74
|
+
BATCH_SIZE = 100 # max 100 per call
|
|
75
|
+
|
|
76
|
+
# Chunk documents into batches of 100
|
|
77
|
+
batches = [documents[i:i+BATCH_SIZE] for i in range(0, len(documents), BATCH_SIZE)]
|
|
78
|
+
|
|
79
|
+
for batch in batches:
|
|
80
|
+
texts = [doc.text for doc in batch]
|
|
81
|
+
embeddings = client.embed_batch(texts) # 1 call for up to 100 texts
|
|
82
|
+
|
|
83
|
+
for doc, embedding in zip(batch, embeddings):
|
|
84
|
+
store_embedding(doc.id, embedding)
|
|
85
|
+
|
|
86
|
+
# Cost: 10 calls × ($0.10 / 1000 calls) = $0.001 (100x cheaper)
|
|
87
|
+
# Throughput: 10 round trips vs 1000
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// TypeScript — batch embedding: 100x cost reduction
|
|
92
|
+
import { Schift } from '@schift-io/sdk';
|
|
93
|
+
|
|
94
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
95
|
+
|
|
96
|
+
const documents = await loadDocuments(); // 1000 documents
|
|
97
|
+
const BATCH_SIZE = 100;
|
|
98
|
+
|
|
99
|
+
// Helper: split array into chunks
|
|
100
|
+
function chunk<T>(arr: T[], size: number): T[][] {
|
|
101
|
+
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
|
|
102
|
+
arr.slice(i * size, i * size + size)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const batches = chunk(documents, BATCH_SIZE);
|
|
107
|
+
|
|
108
|
+
for (const batch of batches) {
|
|
109
|
+
const texts = batch.map(doc => doc.text);
|
|
110
|
+
const embeddings = await client.embedBatch(texts); // 1 call for up to 100 texts
|
|
111
|
+
|
|
112
|
+
await Promise.all(
|
|
113
|
+
batch.map((doc, i) => storeEmbedding(doc.id, embeddings[i]))
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Cost: 10 billing units = $0.001 (vs $0.10 for individual calls)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Cost comparison at scale:**
|
|
121
|
+
|
|
122
|
+
| Approach | 1K texts | 10K texts | 100K texts |
|
|
123
|
+
|----------|----------|-----------|------------|
|
|
124
|
+
| 1 call per text | $0.10 | $1.00 | $10.00 |
|
|
125
|
+
| Batch of 100 | $0.001 | $0.01 | $0.10 |
|
|
126
|
+
| **Savings** | **99%** | **99%** | **99%** |
|
|
127
|
+
|
|
128
|
+
Also cache embeddings client-side for repeated queries to avoid redundant API calls entirely:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# Python — client-side embedding cache for repeated queries
|
|
132
|
+
import hashlib
|
|
133
|
+
from functools import lru_cache
|
|
134
|
+
from schift import Schift
|
|
135
|
+
|
|
136
|
+
client = Schift(api_key="sch_...")
|
|
137
|
+
|
|
138
|
+
# Simple in-memory cache (use Redis or a file cache for persistence)
|
|
139
|
+
_embedding_cache: dict[str, list[float]] = {}
|
|
140
|
+
|
|
141
|
+
def embed_with_cache(text: str) -> list[float]:
|
|
142
|
+
key = hashlib.sha256(text.encode()).hexdigest()
|
|
143
|
+
if key not in _embedding_cache:
|
|
144
|
+
_embedding_cache[key] = client.embed(text)
|
|
145
|
+
return _embedding_cache[key]
|
|
146
|
+
|
|
147
|
+
# For search queries that repeat across users (e.g., "what is your refund policy?"),
|
|
148
|
+
# this means the embedding is computed once and reused for all matching queries.
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// TypeScript — client-side embedding cache
|
|
153
|
+
import { createHash } from 'crypto';
|
|
154
|
+
import { Schift } from '@schift-io/sdk';
|
|
155
|
+
|
|
156
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
157
|
+
const cache = new Map<string, number[]>();
|
|
158
|
+
|
|
159
|
+
async function embedWithCache(text: string): Promise<number[]> {
|
|
160
|
+
const key = createHash('sha256').update(text).digest('hex');
|
|
161
|
+
if (!cache.has(key)) {
|
|
162
|
+
cache.set(key, await client.embed(text));
|
|
163
|
+
}
|
|
164
|
+
return cache.get(key)!;
|
|
165
|
+
}
|
|
166
|
+
// Use a TTL cache (e.g., lru-cache with maxAge) for production.
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Batching checklist:**
|
|
170
|
+
|
|
171
|
+
- Use `embed_batch` / `embedBatch` for all bulk operations (indexing, re-indexing, data migration)
|
|
172
|
+
- Use `embed` only for single real-time queries (user search input)
|
|
173
|
+
- Cache query embeddings when the same query may repeat (chatbot, search UI)
|
|
174
|
+
- For very large datasets (>100K texts), process batches concurrently with a concurrency limit of 5–10
|
|
175
|
+
|
|
176
|
+
## Reference
|
|
177
|
+
|
|
178
|
+
- https://docs.schift.io/api/embed-batch
|
|
179
|
+
- https://docs.schift.io/pricing
|