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.
Files changed (40) hide show
  1. schift_cli/__init__.py +1 -0
  2. schift_cli/client.py +119 -0
  3. schift_cli/commands/__init__.py +0 -0
  4. schift_cli/commands/auth.py +68 -0
  5. schift_cli/commands/bench.py +65 -0
  6. schift_cli/commands/catalog.py +74 -0
  7. schift_cli/commands/db.py +96 -0
  8. schift_cli/commands/embed.py +104 -0
  9. schift_cli/commands/migrate.py +127 -0
  10. schift_cli/commands/query.py +66 -0
  11. schift_cli/commands/skill.py +110 -0
  12. schift_cli/commands/usage.py +50 -0
  13. schift_cli/config.py +58 -0
  14. schift_cli/data/schift-best-practices/AGENTS.md +77 -0
  15. schift_cli/data/schift-best-practices/CLAUDE.md +77 -0
  16. schift_cli/data/schift-best-practices/SKILL.md +89 -0
  17. schift_cli/data/schift-best-practices/references/bucket-organization.md +126 -0
  18. schift_cli/data/schift-best-practices/references/bucket-upload.md +116 -0
  19. schift_cli/data/schift-best-practices/references/chatbot-widget.md +238 -0
  20. schift_cli/data/schift-best-practices/references/cost-batching.md +179 -0
  21. schift_cli/data/schift-best-practices/references/cost-storage-tiers.md +183 -0
  22. schift_cli/data/schift-best-practices/references/deploy-cloudrun.md +140 -0
  23. schift_cli/data/schift-best-practices/references/embed-batch-processing.md +86 -0
  24. schift_cli/data/schift-best-practices/references/embed-error-handling.md +155 -0
  25. schift_cli/data/schift-best-practices/references/embed-multimodal.md +100 -0
  26. schift_cli/data/schift-best-practices/references/embed-task-types.md +135 -0
  27. schift_cli/data/schift-best-practices/references/rag-chunking.md +173 -0
  28. schift_cli/data/schift-best-practices/references/rag-workflow-builder.md +205 -0
  29. schift_cli/data/schift-best-practices/references/sdk-async-patterns.md +103 -0
  30. schift_cli/data/schift-best-practices/references/sdk-auth-patterns.md +76 -0
  31. schift_cli/data/schift-best-practices/references/search-collection-design.md +229 -0
  32. schift_cli/data/schift-best-practices/references/search-hybrid.md +163 -0
  33. schift_cli/data/schift-best-practices/references/search-similarity-tuning.md +134 -0
  34. schift_cli/display.py +85 -0
  35. schift_cli/main.py +39 -0
  36. schift_cli-0.1.0.dist-info/METADATA +12 -0
  37. schift_cli-0.1.0.dist-info/RECORD +40 -0
  38. schift_cli-0.1.0.dist-info/WHEEL +5 -0
  39. schift_cli-0.1.0.dist-info/entry_points.txt +2 -0
  40. 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