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,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Understand Storage Tier Lifecycle to Optimize Costs
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Using the wrong storage tier causes either data loss (Free tier deletes S3 data after 30 days) or unnecessary cost (storing cold-access data in FAISS hot storage). Matching tier to access pattern is essential for production reliability.
|
|
5
|
+
tags:
|
|
6
|
+
- cost
|
|
7
|
+
- storage
|
|
8
|
+
- tiers
|
|
9
|
+
- lifecycle
|
|
10
|
+
- faiss
|
|
11
|
+
- s3
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Understand Storage Tier Lifecycle to Optimize Costs
|
|
15
|
+
|
|
16
|
+
Schift stores your vector data in two layers: FAISS (hot, in-memory vector index for fast search) and S3 (durable object storage for the original chunks and metadata). Each pricing tier has different retention policies for both layers. Mismatching your use case to the wrong tier causes data loss or unnecessary cost.
|
|
17
|
+
|
|
18
|
+
The critical mistake is treating the Free tier as suitable for production data — S3 data is deleted after 30 days, and once deleted, your collection cannot be rebuilt without re-uploading and re-embedding all source documents.
|
|
19
|
+
|
|
20
|
+
### Storage Tier Reference
|
|
21
|
+
|
|
22
|
+
| Tier | FAISS Hot Retention | S3 Retention | Use Case |
|
|
23
|
+
|------|--------------------|--------------|-|
|
|
24
|
+
| Free | 7 days | Deleted after 30 days | Development, demos, prototypes |
|
|
25
|
+
| Pro | 30 days | → IA after 30d → Glacier after 90d | Production applications |
|
|
26
|
+
| Enterprise | 90 days | → IA after 90d | High-frequency search workloads |
|
|
27
|
+
|
|
28
|
+
**Key terms:**
|
|
29
|
+
- **FAISS hot**: Vector index loaded in memory — sub-millisecond search latency
|
|
30
|
+
- **FAISS cold**: Index evicted from memory, reloaded on next query (adds ~1–3s warmup)
|
|
31
|
+
- **S3 Standard**: Instant retrieval, billed at standard rate
|
|
32
|
+
- **S3 IA** (Infrequent Access): Lower storage cost, slightly higher retrieval cost
|
|
33
|
+
- **S3 Glacier**: Cheapest storage, minutes-to-hours retrieval time (not suitable for live search)
|
|
34
|
+
|
|
35
|
+
### Incorrect
|
|
36
|
+
|
|
37
|
+
Using Free tier for production data — collection becomes unreachable after 30 days:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
# Python — Free tier for production: S3 data deleted at day 30
|
|
41
|
+
from schift import Schift
|
|
42
|
+
|
|
43
|
+
# Free tier API key used in production application
|
|
44
|
+
client = Schift(api_key="sch_free_...")
|
|
45
|
+
|
|
46
|
+
# Upload 50,000 documents — 3 months of engineering work
|
|
47
|
+
result = client.bucket.upload_bulk(bucket_id, documents)
|
|
48
|
+
print(f"Indexed {result.chunk_count} chunks")
|
|
49
|
+
|
|
50
|
+
# Day 7: FAISS evicted — first query adds 2s cold-start latency (no warning)
|
|
51
|
+
# Day 30: S3 data deleted — collection.search() returns empty results with no error
|
|
52
|
+
# Day 31: client.search(collection_id, query) → [] (silently empty, data gone)
|
|
53
|
+
|
|
54
|
+
# There is no recovery path. Source documents must be re-uploaded.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// TypeScript — Free tier with no monitoring: invisible data expiry
|
|
59
|
+
import { Schift } from '@schift-io/sdk';
|
|
60
|
+
|
|
61
|
+
const client = new Schift({ apiKey: 'sch_free_...' }); // Free tier
|
|
62
|
+
|
|
63
|
+
// Build a production chatbot — works fine for 29 days
|
|
64
|
+
const answer = await client.workflow.run(workflowId, { query: userQuestion });
|
|
65
|
+
|
|
66
|
+
// Day 30+: search returns no results, LLM generates hallucinated answers
|
|
67
|
+
// because context is empty. No exception thrown — just silent data loss.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Correct
|
|
71
|
+
|
|
72
|
+
Use Pro for production data; pin frequently-accessed collections to hot storage:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# Python — Pro tier with explicit tier awareness
|
|
76
|
+
from schift import Schift
|
|
77
|
+
|
|
78
|
+
client = Schift(api_key="sch_pro_...") # Pro tier
|
|
79
|
+
|
|
80
|
+
# Create collection with tier-appropriate settings
|
|
81
|
+
collection = client.collection.create(
|
|
82
|
+
name="docs-prod",
|
|
83
|
+
tier="pro", # 30d FAISS hot, S3 → IA → Glacier lifecycle
|
|
84
|
+
region="ap-northeast-2",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Pin high-traffic collections to hot storage indefinitely
|
|
88
|
+
# (overrides the 30-day FAISS eviction, billed at hot storage rate)
|
|
89
|
+
client.collection.pin(collection.id, hot=True)
|
|
90
|
+
|
|
91
|
+
# Upload production data — durable beyond 90 days
|
|
92
|
+
result = client.bucket.upload_bulk(
|
|
93
|
+
bucket_id,
|
|
94
|
+
documents,
|
|
95
|
+
metadata={"env": "production", "indexed_at": "2026-03-17"}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Monitor cold-start risk: check FAISS warmth before high-stakes queries
|
|
99
|
+
status = client.collection.status(collection.id)
|
|
100
|
+
if not status.faiss_hot:
|
|
101
|
+
client.collection.warm(collection.id) # pre-warm before query
|
|
102
|
+
# warm() is async — poll status or add a short wait for latency-sensitive paths
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// TypeScript — Pro tier with lifecycle-aware patterns
|
|
107
|
+
import { Schift } from '@schift-io/sdk';
|
|
108
|
+
|
|
109
|
+
const client = new Schift({ apiKey: 'sch_pro_...' }); // Pro tier
|
|
110
|
+
|
|
111
|
+
// Setup: create production collection with hot pin for frequently searched data
|
|
112
|
+
async function setupProductionCollection() {
|
|
113
|
+
const collection = await client.collection.create({
|
|
114
|
+
name: 'docs-prod',
|
|
115
|
+
tier: 'pro',
|
|
116
|
+
region: 'ap-northeast-2',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Pin to hot storage if this collection is queried frequently (>100 queries/day)
|
|
120
|
+
await client.collection.pin(collection.id, { hot: true });
|
|
121
|
+
|
|
122
|
+
return collection.id;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Runtime: check warmth before user-facing queries to avoid cold-start surprises
|
|
126
|
+
async function searchWithWarmGuard(collectionId: string, query: string) {
|
|
127
|
+
const status = await client.collection.status(collectionId);
|
|
128
|
+
|
|
129
|
+
if (!status.faissHot) {
|
|
130
|
+
// Trigger warm-up for low-latency access on next query
|
|
131
|
+
// For latency-sensitive paths, await the warm before proceeding
|
|
132
|
+
await client.collection.warm(collectionId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return client.search(collectionId, query, { topK: 5 });
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Tier selection guide:**
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# Python — choose tier based on access pattern
|
|
143
|
+
def choose_tier(access_frequency: str, data_criticality: str) -> str:
|
|
144
|
+
"""
|
|
145
|
+
access_frequency: "daily" | "weekly" | "monthly" | "one-time"
|
|
146
|
+
data_criticality: "production" | "staging" | "dev"
|
|
147
|
+
"""
|
|
148
|
+
if data_criticality == "dev":
|
|
149
|
+
return "free" # OK to lose after 30 days
|
|
150
|
+
|
|
151
|
+
if access_frequency == "daily":
|
|
152
|
+
return "pro" # FAISS stays hot 30d, pin for longer
|
|
153
|
+
|
|
154
|
+
if access_frequency in ("weekly", "monthly") and data_criticality == "production":
|
|
155
|
+
return "pro" # S3 lifecycle keeps data, FAISS cold-starts acceptable
|
|
156
|
+
|
|
157
|
+
if access_frequency == "one-time":
|
|
158
|
+
return "free" # Temporary indexing, no need for retention
|
|
159
|
+
|
|
160
|
+
return "pro" # Default to Pro for any production use
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Cost impact of correct tier usage (10GB collection):**
|
|
164
|
+
|
|
165
|
+
| Tier | Storage Cost/Month | Risk |
|
|
166
|
+
|------|--------------------|------|
|
|
167
|
+
| Free | $0 | Data deleted at day 30 |
|
|
168
|
+
| Pro | $1.50 (10GB × $0.15) | Safe for production |
|
|
169
|
+
| Enterprise | Custom | High-frequency, SLA-backed |
|
|
170
|
+
|
|
171
|
+
**Production checklist:**
|
|
172
|
+
|
|
173
|
+
- Never use a Free tier key in any environment where data loss is unacceptable
|
|
174
|
+
- Pin collections to hot storage if queried more than 50 times per day
|
|
175
|
+
- Monitor `collection.status().faissHot` before latency-sensitive query paths
|
|
176
|
+
- Set up alerts for S3 lifecycle transitions if you use collections infrequently
|
|
177
|
+
- Store collection IDs and creation dates — helps audit what tier data lives on
|
|
178
|
+
|
|
179
|
+
## Reference
|
|
180
|
+
|
|
181
|
+
- https://docs.schift.io/storage/tiers
|
|
182
|
+
- https://docs.schift.io/storage/lifecycle
|
|
183
|
+
- https://docs.schift.io/pricing
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Deploy Schift-Powered Apps on Cloud Run for Zero Idle Cost
|
|
3
|
+
impact: LOW-MEDIUM
|
|
4
|
+
impactDescription: Zero idle cost with proper Cloud Run configuration
|
|
5
|
+
tags:
|
|
6
|
+
- deployment
|
|
7
|
+
- cloud-run
|
|
8
|
+
- docker
|
|
9
|
+
- infrastructure
|
|
10
|
+
- cost
|
|
11
|
+
- scaling
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Deploy Schift-Powered Apps on Cloud Run for Zero Idle Cost
|
|
15
|
+
|
|
16
|
+
Cloud Run scales to zero when no traffic is coming in, so you pay nothing while your app is idle. This is ideal for Schift-backed services that handle sporadic traffic — chatbots, search APIs, document Q&A endpoints. The common mistake is deploying these on an always-on VM, which bills 24/7 regardless of actual usage.
|
|
17
|
+
|
|
18
|
+
### Incorrect
|
|
19
|
+
|
|
20
|
+
Running a Schift-powered chatbot on an always-on VM that pays for compute even during zero-traffic hours:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Always-on EC2 / Compute Engine VM approach
|
|
24
|
+
# Costs ~$15–30/month even when nobody is using the app
|
|
25
|
+
# Manual scaling, manual restarts, no request-based autoscaling
|
|
26
|
+
|
|
27
|
+
ssh ec2-user@your-server
|
|
28
|
+
pm2 start server.js # just stays running forever
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// server.ts — no health check, no graceful shutdown
|
|
33
|
+
// Deployed on an always-on VM: billed 24/7
|
|
34
|
+
import { Hono } from 'hono';
|
|
35
|
+
import { Schift } from '@schift-io/sdk';
|
|
36
|
+
|
|
37
|
+
const app = new Hono();
|
|
38
|
+
const client = new Schift({ apiKey: process.env.SCHIFT_API_KEY! });
|
|
39
|
+
|
|
40
|
+
app.post('/chat', async (c) => {
|
|
41
|
+
const { query } = await c.req.json();
|
|
42
|
+
const results = await client.search('col_...', query, { topK: 3 });
|
|
43
|
+
return c.json({ results });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export default app;
|
|
47
|
+
// No /health endpoint → load balancer cannot confirm readiness
|
|
48
|
+
// No memory limit set → OOM-killed silently
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This approach pays for a full VM instance even when traffic is zero, lacks health checks for Cloud Run readiness probes, and requires manual scaling during traffic spikes.
|
|
52
|
+
|
|
53
|
+
### Correct
|
|
54
|
+
|
|
55
|
+
Deploy on Cloud Run with `min-instances=0`, set memory for embedding operations, and expose a `/health` endpoint:
|
|
56
|
+
|
|
57
|
+
```dockerfile
|
|
58
|
+
# Dockerfile — multi-stage build for a minimal runtime image
|
|
59
|
+
FROM node:20-alpine AS builder
|
|
60
|
+
WORKDIR /app
|
|
61
|
+
COPY package*.json ./
|
|
62
|
+
RUN npm ci --only=production
|
|
63
|
+
COPY . .
|
|
64
|
+
RUN npm run build
|
|
65
|
+
|
|
66
|
+
FROM node:20-alpine AS runtime
|
|
67
|
+
WORKDIR /app
|
|
68
|
+
# Copy only compiled output and production deps
|
|
69
|
+
COPY --from=builder /app/dist ./dist
|
|
70
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
71
|
+
COPY package.json ./
|
|
72
|
+
|
|
73
|
+
# Cloud Run injects PORT; default to 8080
|
|
74
|
+
ENV PORT=8080
|
|
75
|
+
EXPOSE 8080
|
|
76
|
+
|
|
77
|
+
CMD ["node", "dist/server.js"]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// server.ts — production-ready Cloud Run handler
|
|
82
|
+
import { Hono } from 'hono';
|
|
83
|
+
import { serve } from '@hono/node-server';
|
|
84
|
+
import { Schift } from '@schift-io/sdk';
|
|
85
|
+
|
|
86
|
+
const app = new Hono();
|
|
87
|
+
|
|
88
|
+
// SCHIFT_API_KEY is injected from Cloud Run secret at runtime
|
|
89
|
+
const client = new Schift({ apiKey: process.env.SCHIFT_API_KEY! });
|
|
90
|
+
|
|
91
|
+
// Required: Cloud Run sends readiness/liveness probes to /health
|
|
92
|
+
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
93
|
+
|
|
94
|
+
app.post('/search', async (c) => {
|
|
95
|
+
const { query, collectionId } = await c.req.json();
|
|
96
|
+
const results = await client.search(collectionId, query, {
|
|
97
|
+
topK: 5,
|
|
98
|
+
scoreThreshold: 0.72,
|
|
99
|
+
});
|
|
100
|
+
return c.json({ results });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const port = Number(process.env.PORT) || 8080;
|
|
104
|
+
serve({ fetch: app.fetch, port });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Build and push the container image
|
|
109
|
+
gcloud builds submit --tag gcr.io/YOUR_PROJECT/schift-app
|
|
110
|
+
|
|
111
|
+
# Deploy to Cloud Run
|
|
112
|
+
gcloud run deploy schift-app \
|
|
113
|
+
--image gcr.io/YOUR_PROJECT/schift-app \
|
|
114
|
+
--platform managed \
|
|
115
|
+
--region us-central1 \
|
|
116
|
+
--allow-unauthenticated \
|
|
117
|
+
--min-instances 0 \ # scale to zero → $0 idle cost
|
|
118
|
+
--max-instances 10 \
|
|
119
|
+
--concurrency 80 \ # requests handled per instance before scaling out
|
|
120
|
+
--timeout 300 \ # 5 min max for long embedding/search ops
|
|
121
|
+
--memory 512Mi \ # minimum for embedding operations
|
|
122
|
+
--set-secrets SCHIFT_API_KEY=schift-api-key:latest # inject from Secret Manager
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Key settings explained:**
|
|
126
|
+
|
|
127
|
+
| Setting | Value | Why |
|
|
128
|
+
|---------|-------|-----|
|
|
129
|
+
| `min-instances` | `0` | Scales to zero when idle — $0 cost |
|
|
130
|
+
| `concurrency` | `80` | Cloud Run default; safe for I/O-bound search APIs |
|
|
131
|
+
| `timeout` | `300s` | Allows time for cold starts + embedding latency |
|
|
132
|
+
| `memory` | `512Mi` | Minimum for Schift SDK + embedding response buffers |
|
|
133
|
+
| `SCHIFT_API_KEY` | Secret Manager | Never hardcode API keys in the image |
|
|
134
|
+
|
|
135
|
+
**Cold start note:** With `min-instances=0`, the first request after idle incurs a cold start (~1–3 seconds). If your use case requires consistent low latency, set `min-instances=1` — this costs ~$5–10/month but eliminates cold starts.
|
|
136
|
+
|
|
137
|
+
## Reference
|
|
138
|
+
|
|
139
|
+
- https://docs.schift.io/deployment/cloud-run
|
|
140
|
+
- https://cloud.google.com/run/docs/configuring/min-instances
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use embed_batch() for multiple texts
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Looping embed() one-by-one produces 10-50x more API calls than a single embed_batch() call, increasing latency and cost proportionally.
|
|
5
|
+
tags: [embedding, performance, cost, batch]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use embed_batch() for multiple texts
|
|
9
|
+
|
|
10
|
+
When embedding more than one piece of text, always use `embed_batch()` instead of calling `embed()` in a loop. Each `embed()` call is a separate HTTP round-trip. `embed_batch()` sends all texts in a single request (up to 100 per call) and returns embeddings in the same order.
|
|
11
|
+
|
|
12
|
+
**Incorrect** — calling `embed()` in a loop creates one HTTP request per text:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
# Python - DON'T do this
|
|
16
|
+
from schift import Schift
|
|
17
|
+
|
|
18
|
+
client = Schift(api_key="sch_...")
|
|
19
|
+
texts = ["cat", "dog", "bird", "fish"]
|
|
20
|
+
|
|
21
|
+
embeddings = []
|
|
22
|
+
for text in texts:
|
|
23
|
+
result = client.embed(text) # 4 separate API calls
|
|
24
|
+
embeddings.append(result.embedding)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// TypeScript - DON'T do this
|
|
29
|
+
import { Schift } from '@schift-io/sdk';
|
|
30
|
+
|
|
31
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
32
|
+
const texts = ['cat', 'dog', 'bird', 'fish'];
|
|
33
|
+
|
|
34
|
+
const embeddings = [];
|
|
35
|
+
for (const text of texts) {
|
|
36
|
+
const result = await client.embed(text); // 4 separate API calls
|
|
37
|
+
embeddings.push(result.embedding);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Correct** — one `embed_batch()` call handles the entire list:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# Python - DO this
|
|
45
|
+
from schift import Schift
|
|
46
|
+
|
|
47
|
+
client = Schift(api_key="sch_...")
|
|
48
|
+
texts = ["cat", "dog", "bird", "fish"]
|
|
49
|
+
|
|
50
|
+
results = client.embed_batch(texts) # 1 API call
|
|
51
|
+
embeddings = [r.embedding for r in results] # same order as input
|
|
52
|
+
|
|
53
|
+
# For large datasets, chunk into batches of up to 100
|
|
54
|
+
BATCH_SIZE = 100
|
|
55
|
+
all_embeddings = []
|
|
56
|
+
for i in range(0, len(texts), BATCH_SIZE):
|
|
57
|
+
batch = texts[i : i + BATCH_SIZE]
|
|
58
|
+
results = client.embed_batch(batch)
|
|
59
|
+
all_embeddings.extend(r.embedding for r in results)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// TypeScript - DO this
|
|
64
|
+
import { Schift } from '@schift-io/sdk';
|
|
65
|
+
|
|
66
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
67
|
+
const texts = ['cat', 'dog', 'bird', 'fish'];
|
|
68
|
+
|
|
69
|
+
const results = await client.embedBatch(texts); // 1 API call
|
|
70
|
+
const embeddings = results.map((r) => r.embedding); // same order as input
|
|
71
|
+
|
|
72
|
+
// For large datasets, chunk into batches of up to 100
|
|
73
|
+
const BATCH_SIZE = 100;
|
|
74
|
+
const allEmbeddings: number[][] = [];
|
|
75
|
+
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
|
76
|
+
const batch = texts.slice(i, i + BATCH_SIZE);
|
|
77
|
+
const batchResults = await client.embedBatch(batch);
|
|
78
|
+
allEmbeddings.push(...batchResults.map((r) => r.embedding));
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The `embed_batch()` / `embedBatch()` limit is 100 texts per call. For larger datasets, split into chunks of 100 as shown above. Results are always returned in the same order as the input list.
|
|
83
|
+
|
|
84
|
+
## Reference
|
|
85
|
+
|
|
86
|
+
- https://docs.schift.io/api/embed#batch
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Handle AuthError, QuotaError, and rate limits properly
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Unhandled quota or rate-limit errors crash production jobs and waste partially processed batches. Proper error handling with exponential backoff recovers automatically and avoids re-processing already-embedded documents.
|
|
5
|
+
tags: [embedding, error-handling, rate-limits, quota, retry]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Handle AuthError, QuotaError, and rate limits properly
|
|
9
|
+
|
|
10
|
+
Schift raises three primary error types you must handle:
|
|
11
|
+
|
|
12
|
+
| Error | HTTP status | Cause |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `AuthError` | 401 | Invalid or missing API key |
|
|
15
|
+
| `QuotaError` | 402 | Monthly credit limit reached |
|
|
16
|
+
| `RateLimitError` | 429 | Too many requests per second |
|
|
17
|
+
|
|
18
|
+
`AuthError` is a hard failure — check your key. `QuotaError` means you need to top up credits or wait for the next billing cycle. `RateLimitError` is transient and should be retried with exponential backoff.
|
|
19
|
+
|
|
20
|
+
**Incorrect** — no error handling; a single quota error crashes the entire job:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
# Python - DON'T do this
|
|
24
|
+
from schift import Schift
|
|
25
|
+
|
|
26
|
+
client = Schift(api_key="sch_...")
|
|
27
|
+
texts = load_large_dataset() # 10,000 documents
|
|
28
|
+
|
|
29
|
+
# If quota runs out halfway through, the whole job crashes
|
|
30
|
+
# and there is no way to resume from where it stopped
|
|
31
|
+
results = client.embed_batch(texts)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// TypeScript - DON'T do this
|
|
36
|
+
import { Schift } from '@schift-io/sdk';
|
|
37
|
+
|
|
38
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
39
|
+
const texts = await loadLargeDataset();
|
|
40
|
+
|
|
41
|
+
// Unhandled promise rejection on quota exceeded
|
|
42
|
+
const results = await client.embedBatch(texts);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Correct** — catch specific errors and apply exponential backoff for rate limits:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# Python - DO this
|
|
49
|
+
import time
|
|
50
|
+
from schift import Schift, AuthError, QuotaError, SchiftError
|
|
51
|
+
|
|
52
|
+
client = Schift(api_key="sch_...")
|
|
53
|
+
|
|
54
|
+
def embed_with_retry(texts: list[str], max_retries: int = 5) -> list:
|
|
55
|
+
"""Embed texts with exponential backoff on rate limit errors."""
|
|
56
|
+
for attempt in range(max_retries):
|
|
57
|
+
try:
|
|
58
|
+
return client.embed_batch(texts)
|
|
59
|
+
|
|
60
|
+
except AuthError:
|
|
61
|
+
# Hard failure — wrong API key, do not retry
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
"Invalid Schift API key. Check your SCHIFT_API_KEY env var."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
except QuotaError as e:
|
|
67
|
+
# Credits exhausted — log and stop; retrying won't help
|
|
68
|
+
raise RuntimeError(
|
|
69
|
+
f"Schift quota exceeded. Top up credits at https://app.schift.io/billing. "
|
|
70
|
+
f"Details: {e}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
except SchiftError as e:
|
|
74
|
+
if e.status_code == 429:
|
|
75
|
+
# Rate limit — wait and retry with exponential backoff
|
|
76
|
+
wait = 2 ** attempt # 1s, 2s, 4s, 8s, 16s
|
|
77
|
+
print(f"Rate limited. Retrying in {wait}s (attempt {attempt + 1}/{max_retries})")
|
|
78
|
+
time.sleep(wait)
|
|
79
|
+
else:
|
|
80
|
+
raise # Unexpected error — surface it
|
|
81
|
+
|
|
82
|
+
raise RuntimeError(f"embed_with_retry failed after {max_retries} attempts")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Check remaining credits before starting a large batch job
|
|
86
|
+
def safe_batch_embed(texts: list[str]) -> list:
|
|
87
|
+
account = client.account.get()
|
|
88
|
+
if account.credits_remaining < len(texts):
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
f"Insufficient credits: need {len(texts)}, have {account.credits_remaining}"
|
|
91
|
+
)
|
|
92
|
+
return embed_with_retry(texts)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// TypeScript - DO this
|
|
97
|
+
import { Schift, AuthError, QuotaError, SchiftError } from '@schift-io/sdk';
|
|
98
|
+
|
|
99
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
100
|
+
|
|
101
|
+
async function embedWithRetry(
|
|
102
|
+
texts: string[],
|
|
103
|
+
maxRetries = 5,
|
|
104
|
+
): Promise<{ embedding: number[] }[]> {
|
|
105
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
106
|
+
try {
|
|
107
|
+
return await client.embedBatch(texts);
|
|
108
|
+
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err instanceof AuthError) {
|
|
111
|
+
// Hard failure — wrong API key, do not retry
|
|
112
|
+
throw new Error(
|
|
113
|
+
'Invalid Schift API key. Check your SCHIFT_API_KEY env var.',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (err instanceof QuotaError) {
|
|
118
|
+
// Credits exhausted — log and stop
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Schift quota exceeded. Top up credits at https://app.schift.io/billing. ${err.message}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (err instanceof SchiftError && err.statusCode === 429) {
|
|
125
|
+
// Rate limit — exponential backoff
|
|
126
|
+
const wait = 2 ** attempt * 1000; // 1s, 2s, 4s, 8s, 16s
|
|
127
|
+
console.warn(`Rate limited. Retrying in ${wait / 1000}s (attempt ${attempt + 1}/${maxRetries})`);
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw err; // Unexpected error — surface it
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`embedWithRetry failed after ${maxRetries} attempts`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check credits before a large batch job
|
|
139
|
+
async function safeBatchEmbed(texts: string[]) {
|
|
140
|
+
const account = await client.account.get();
|
|
141
|
+
if (account.creditsRemaining < texts.length) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Insufficient credits: need ${texts.length}, have ${account.creditsRemaining}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return embedWithRetry(texts);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
For long-running indexing jobs, track which documents have already been embedded (e.g. store their IDs in a database) so you can resume from where you left off after a `QuotaError` rather than restarting from scratch.
|
|
151
|
+
|
|
152
|
+
## Reference
|
|
153
|
+
|
|
154
|
+
- https://docs.schift.io/api/errors
|
|
155
|
+
- https://docs.schift.io/guides/rate-limits
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Use modality parameter for non-text inputs
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Passing raw image bytes or file content as a plain text string produces garbage embeddings. The modality parameter routes the input through the correct encoder before projection into canonical space.
|
|
5
|
+
tags: [embedding, multimodal, image, document, audio]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Use modality parameter for non-text inputs
|
|
9
|
+
|
|
10
|
+
Schift supports five modalities: `text`, `image`, `audio`, `video`, and `document`. Every modality is projected into the same canonical 1024-dimensional space, so cross-modal search (e.g. text query against image collection) works out of the box — but only if you specify the correct `modality` when embedding.
|
|
11
|
+
|
|
12
|
+
Passing raw binary content without setting `modality` causes Schift to treat the input as UTF-8 text, producing meaningless embeddings.
|
|
13
|
+
|
|
14
|
+
**Incorrect** — passing image bytes as plain text produces garbage vectors:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# Python - DON'T do this
|
|
18
|
+
from schift import Schift
|
|
19
|
+
|
|
20
|
+
client = Schift(api_key="sch_...")
|
|
21
|
+
|
|
22
|
+
with open("photo.jpg", "rb") as f:
|
|
23
|
+
image_bytes = f.read()
|
|
24
|
+
|
|
25
|
+
# Wrong: embed() with no modality treats input as text
|
|
26
|
+
result = client.embed(image_bytes) # garbage embedding
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// TypeScript - DON'T do this
|
|
31
|
+
import { Schift } from '@schift-io/sdk';
|
|
32
|
+
import { readFileSync } from 'fs';
|
|
33
|
+
|
|
34
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
35
|
+
|
|
36
|
+
const imageBuffer = readFileSync('photo.jpg');
|
|
37
|
+
|
|
38
|
+
// Wrong: no modality — treated as text
|
|
39
|
+
const result = await client.embed(imageBuffer.toString()); // garbage embedding
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Correct** — specify `modality="image"` (or the appropriate modality) and pass the file path or bytes:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# Python - DO this
|
|
46
|
+
from schift import Schift
|
|
47
|
+
|
|
48
|
+
client = Schift(api_key="sch_...")
|
|
49
|
+
|
|
50
|
+
# Embed a single image
|
|
51
|
+
result = client.embed("photo.jpg", modality="image")
|
|
52
|
+
image_vector = result.embedding # 1024d, same space as text embeddings
|
|
53
|
+
|
|
54
|
+
# Embed a PDF document
|
|
55
|
+
doc_result = client.embed("report.pdf", modality="document")
|
|
56
|
+
|
|
57
|
+
# Cross-modal search: text query against image collection
|
|
58
|
+
text_query = client.embed("a golden retriever running in a park", modality="text")
|
|
59
|
+
# text_query.embedding can now be used to search an image collection directly
|
|
60
|
+
|
|
61
|
+
# Batch embed multiple images
|
|
62
|
+
image_paths = ["img1.jpg", "img2.jpg", "img3.jpg"]
|
|
63
|
+
results = client.embed_batch(image_paths, modality="image")
|
|
64
|
+
vectors = [r.embedding for r in results]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// TypeScript - DO this
|
|
69
|
+
import { Schift } from '@schift-io/sdk';
|
|
70
|
+
|
|
71
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
72
|
+
|
|
73
|
+
// Embed a single image by file path
|
|
74
|
+
const result = await client.embed('photo.jpg', { modality: 'image' });
|
|
75
|
+
const imageVector = result.embedding; // 1024d, same space as text
|
|
76
|
+
|
|
77
|
+
// Embed a PDF document
|
|
78
|
+
const docResult = await client.embed('report.pdf', { modality: 'document' });
|
|
79
|
+
|
|
80
|
+
// Cross-modal: text query against image collection
|
|
81
|
+
const textQuery = await client.embed(
|
|
82
|
+
'a golden retriever running in a park',
|
|
83
|
+
{ modality: 'text' },
|
|
84
|
+
);
|
|
85
|
+
// textQuery.embedding searches image collections directly
|
|
86
|
+
|
|
87
|
+
// Batch embed multiple images
|
|
88
|
+
const imagePaths = ['img1.jpg', 'img2.jpg', 'img3.jpg'];
|
|
89
|
+
const results = await client.embedBatch(imagePaths, { modality: 'image' });
|
|
90
|
+
const vectors = results.map((r) => r.embedding);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Because all modalities share the same canonical 1024d space, you can search across modalities without any additional conversion step. A text embedding and an image embedding are directly comparable with cosine similarity.
|
|
94
|
+
|
|
95
|
+
Supported modality values: `text` (default), `image`, `audio`, `video`, `document`.
|
|
96
|
+
|
|
97
|
+
## Reference
|
|
98
|
+
|
|
99
|
+
- https://docs.schift.io/api/embed#modality
|
|
100
|
+
- https://docs.schift.io/concepts/canonical-space
|