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,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Design Collections by Domain/Use-Case, Not by Data Source
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: A single monolithic collection forces every query to search irrelevant data, bloating latency and degrading result quality. Per-file collections create management overhead and prevent cross-document ranking within a domain. Domain-aligned collections with consistent metadata schemas give you both search quality and operational simplicity.
|
|
5
|
+
tags:
|
|
6
|
+
- collection
|
|
7
|
+
- schema
|
|
8
|
+
- design
|
|
9
|
+
- organization
|
|
10
|
+
- metadata
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Design Collections by Domain/Use-Case, Not by Data Source
|
|
14
|
+
|
|
15
|
+
Collection design is a structural decision that affects search quality, filter performance, and long-term maintainability. The two common anti-patterns are opposites of each other: one giant collection that holds everything (poor signal-to-noise ratio per query), and one collection per uploaded file (combinatorial overhead, no cross-document ranking).
|
|
16
|
+
|
|
17
|
+
The correct approach is to group documents by the queries they serve. A user asking "how do I reset my API key?" should search `support-docs` — not wade through API reference and release notes in the same index.
|
|
18
|
+
|
|
19
|
+
### Incorrect
|
|
20
|
+
|
|
21
|
+
**Anti-pattern A: One monolithic collection**
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
# Python — everything in one collection
|
|
25
|
+
from schift import Schift
|
|
26
|
+
|
|
27
|
+
client = Schift(api_key="sch_...")
|
|
28
|
+
|
|
29
|
+
# Dumping all company knowledge into a single collection
|
|
30
|
+
client.collection.add("everything", documents=[
|
|
31
|
+
{"id": "pd-001", "text": "Product feature: drag-and-drop upload..."},
|
|
32
|
+
{"id": "st-001", "text": "Support ticket: user cannot login..."},
|
|
33
|
+
{"id": "api-001", "text": "POST /v1/embed — Creates embeddings..."},
|
|
34
|
+
{"id": "rn-001", "text": "Release notes v2.3: added batch support..."},
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
# Query now competes against all domains — noisy results
|
|
38
|
+
results = client.search("everything", "how to embed documents")
|
|
39
|
+
# Returns: API reference, support tickets, release notes, all mixed together
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Anti-pattern B: One collection per file**
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# Python — a new collection for every uploaded file
|
|
46
|
+
for filename, content in uploaded_files:
|
|
47
|
+
collection_id = filename.replace(".pdf", "").replace(" ", "-")
|
|
48
|
+
client.collection.create(collection_id, schema={}) # no schema
|
|
49
|
+
client.collection.add(collection_id, documents=[{"id": "doc", "text": content}])
|
|
50
|
+
|
|
51
|
+
# Now you must search 200 collections separately — impossible to rank across files
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// TypeScript — same anti-patterns
|
|
56
|
+
import { Schift } from '@schift-io/sdk';
|
|
57
|
+
|
|
58
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
59
|
+
|
|
60
|
+
// Anti-pattern A: monolithic
|
|
61
|
+
await client.collection.add('everything', { documents: allMyDocuments });
|
|
62
|
+
|
|
63
|
+
// Anti-pattern B: per-file
|
|
64
|
+
for (const file of uploadedFiles) {
|
|
65
|
+
const id = file.name.replace('.pdf', '');
|
|
66
|
+
await client.collection.create(id, { schema: {} }); // no schema
|
|
67
|
+
await client.collection.add(id, { documents: [{ id: 'doc', text: file.content }] });
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Correct
|
|
72
|
+
|
|
73
|
+
Create collections per domain/use-case with a defined metadata schema:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Python — domain-aligned collections with schemas
|
|
77
|
+
from schift import Schift
|
|
78
|
+
|
|
79
|
+
client = Schift(api_key="sch_...")
|
|
80
|
+
|
|
81
|
+
# Define schema for each domain at creation — only these fields are filter-indexed
|
|
82
|
+
client.collection.create("product-docs", schema={
|
|
83
|
+
"fields": {
|
|
84
|
+
"version": {"type": "string", "filterable": True},
|
|
85
|
+
"category": {"type": "string", "filterable": True}, # "feature", "guide", "faq"
|
|
86
|
+
"updated_at": {"type": "date", "filterable": True},
|
|
87
|
+
"language": {"type": "string", "filterable": True},
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
client.collection.create("support-tickets", schema={
|
|
92
|
+
"fields": {
|
|
93
|
+
"status": {"type": "string", "filterable": True}, # "open", "closed"
|
|
94
|
+
"priority": {"type": "string", "filterable": True},
|
|
95
|
+
"created_at": {"type": "date", "filterable": True},
|
|
96
|
+
"product_area": {"type": "string", "filterable": True},
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
client.collection.create("api-reference", schema={
|
|
101
|
+
"fields": {
|
|
102
|
+
"endpoint": {"type": "string", "filterable": True},
|
|
103
|
+
"method": {"type": "string", "filterable": True}, # "GET", "POST", etc.
|
|
104
|
+
"version": {"type": "string", "filterable": True},
|
|
105
|
+
"deprecated": {"type": "boolean", "filterable": True},
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
# Ingest respects the schema — metadata is validated at write time
|
|
110
|
+
client.collection.add("product-docs", documents=[
|
|
111
|
+
{
|
|
112
|
+
"id": "pd-drag-drop-001",
|
|
113
|
+
"text": "Drag-and-drop upload lets you add files directly to a bucket...",
|
|
114
|
+
"metadata": {
|
|
115
|
+
"version": "2.3",
|
|
116
|
+
"category": "feature",
|
|
117
|
+
"updated_at": "2026-02-10",
|
|
118
|
+
"language": "en",
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
# Queries are scoped to the right domain — clean, relevant results
|
|
124
|
+
product_results = client.search(
|
|
125
|
+
"product-docs",
|
|
126
|
+
"how to upload files",
|
|
127
|
+
top_k=5,
|
|
128
|
+
filters={"category": "guide", "language": "en"}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
support_results = client.search(
|
|
132
|
+
"support-tickets",
|
|
133
|
+
"upload fails with 413 error",
|
|
134
|
+
top_k=8,
|
|
135
|
+
filters={"status": "open", "product_area": "ingestion"}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
api_results = client.search(
|
|
139
|
+
"api-reference",
|
|
140
|
+
"create embedding endpoint",
|
|
141
|
+
top_k=3,
|
|
142
|
+
filters={"deprecated": False, "version": "v1"}
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// TypeScript — domain-aligned collections with schemas
|
|
148
|
+
import { Schift } from '@schift-io/sdk';
|
|
149
|
+
|
|
150
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
151
|
+
|
|
152
|
+
// Create collections with explicit schemas
|
|
153
|
+
await client.collection.create('product-docs', {
|
|
154
|
+
schema: {
|
|
155
|
+
fields: {
|
|
156
|
+
version: { type: 'string', filterable: true },
|
|
157
|
+
category: { type: 'string', filterable: true },
|
|
158
|
+
updated_at: { type: 'date', filterable: true },
|
|
159
|
+
language: { type: 'string', filterable: true },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await client.collection.create('support-tickets', {
|
|
165
|
+
schema: {
|
|
166
|
+
fields: {
|
|
167
|
+
status: { type: 'string', filterable: true },
|
|
168
|
+
priority: { type: 'string', filterable: true },
|
|
169
|
+
created_at: { type: 'date', filterable: true },
|
|
170
|
+
product_area: { type: 'string', filterable: true },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await client.collection.create('api-reference', {
|
|
176
|
+
schema: {
|
|
177
|
+
fields: {
|
|
178
|
+
endpoint: { type: 'string', filterable: true },
|
|
179
|
+
method: { type: 'string', filterable: true },
|
|
180
|
+
version: { type: 'string', filterable: true },
|
|
181
|
+
deprecated: { type: 'boolean', filterable: true },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Ingest with required metadata fields
|
|
187
|
+
await client.collection.add('product-docs', {
|
|
188
|
+
documents: [
|
|
189
|
+
{
|
|
190
|
+
id: 'pd-drag-drop-001',
|
|
191
|
+
text: 'Drag-and-drop upload lets you add files directly to a bucket...',
|
|
192
|
+
metadata: {
|
|
193
|
+
version: '2.3',
|
|
194
|
+
category: 'feature',
|
|
195
|
+
updated_at: '2026-02-10',
|
|
196
|
+
language: 'en',
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Query scoped to the right domain
|
|
203
|
+
const productResults = await client.search('product-docs', 'how to upload files', {
|
|
204
|
+
topK: 5,
|
|
205
|
+
filters: { category: 'guide', language: 'en' },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const supportResults = await client.search('support-tickets', 'upload fails with 413 error', {
|
|
209
|
+
topK: 8,
|
|
210
|
+
filters: { status: 'open', product_area: 'ingestion' },
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Collection design guidelines:**
|
|
215
|
+
|
|
216
|
+
| Principle | Rationale |
|
|
217
|
+
|-----------|-----------|
|
|
218
|
+
| One collection per domain/product area | Keeps semantic similarity scores meaningful within a shared context |
|
|
219
|
+
| Define schema before ingesting | Filterable fields must be declared at creation; adding them later requires re-indexing |
|
|
220
|
+
| Use consistent metadata keys | Inconsistent keys (`doc_type` vs `docType`) break filter queries silently |
|
|
221
|
+
| Aim for 10k–1M documents per collection | Smaller: consider merging. Larger: consider sharding by time or sub-domain |
|
|
222
|
+
| Name collections for query intent | `product-docs` not `confluence-export-2025` |
|
|
223
|
+
|
|
224
|
+
If a single query would logically need to search across multiple domains (e.g., a universal search bar), run parallel searches against each relevant collection and merge results by score on the client side.
|
|
225
|
+
|
|
226
|
+
## Reference
|
|
227
|
+
|
|
228
|
+
- https://docs.schift.io/collections/schema
|
|
229
|
+
- https://docs.schift.io/collections/design
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Combine Semantic Search with Metadata Filters for Precise Retrieval
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Pure semantic search returns results that are thematically similar but contextually wrong — e.g., querying "quarterly revenue" returns documents from the wrong year or department. Adding metadata filters eliminates these false positives without sacrificing semantic ranking quality.
|
|
5
|
+
tags:
|
|
6
|
+
- search
|
|
7
|
+
- filters
|
|
8
|
+
- metadata
|
|
9
|
+
- hybrid-search
|
|
10
|
+
- retrieval
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Combine Semantic Search with Metadata Filters for Precise Retrieval
|
|
14
|
+
|
|
15
|
+
Semantic search ranks results by vector similarity, which captures meaning well but is blind to structured attributes like dates, departments, document types, or user ownership. Without filters, a query for "quarterly revenue" might surface a 2019 report with slightly higher similarity than the 2025 report you actually need.
|
|
16
|
+
|
|
17
|
+
The `filters` parameter in `client.search()` applies exact-match or range constraints on document metadata before semantic ranking. The semantic model then ranks only the pre-filtered candidates, giving you both precision and relevance.
|
|
18
|
+
|
|
19
|
+
### Incorrect
|
|
20
|
+
|
|
21
|
+
Pure semantic search without filters returns semantically similar but contextually irrelevant results:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
# Python — semantic-only search, no filters
|
|
25
|
+
from schift import Schift
|
|
26
|
+
|
|
27
|
+
client = Schift(api_key="sch_...")
|
|
28
|
+
|
|
29
|
+
# Searching for 2025 finance data — but will return results from any year/department
|
|
30
|
+
results = client.search(
|
|
31
|
+
"finance-reports",
|
|
32
|
+
"quarterly revenue growth",
|
|
33
|
+
top_k=5
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Results may include 2019 marketing reports that mention "revenue" prominently
|
|
37
|
+
for r in results:
|
|
38
|
+
print(r.text, r.score)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// TypeScript — semantic-only search, no filters
|
|
43
|
+
import { Schift } from '@schift-io/sdk';
|
|
44
|
+
|
|
45
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
46
|
+
|
|
47
|
+
// No filters — returns semantically similar but potentially wrong-year docs
|
|
48
|
+
const results = await client.search('finance-reports', 'quarterly revenue growth', {
|
|
49
|
+
topK: 5,
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This wastes top_k slots on irrelevant documents and requires the LLM to reason about document provenance instead of just answering.
|
|
54
|
+
|
|
55
|
+
### Correct
|
|
56
|
+
|
|
57
|
+
Use the `filters` parameter to narrow candidates by metadata before semantic ranking:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# Python — hybrid search with metadata filters
|
|
61
|
+
from schift import Schift
|
|
62
|
+
|
|
63
|
+
client = Schift(api_key="sch_...")
|
|
64
|
+
|
|
65
|
+
# Narrow to 2025 finance department docs, then rank by semantic similarity
|
|
66
|
+
results = client.search(
|
|
67
|
+
"finance-reports",
|
|
68
|
+
"quarterly revenue growth",
|
|
69
|
+
top_k=5,
|
|
70
|
+
filters={
|
|
71
|
+
"year": 2025,
|
|
72
|
+
"department": "finance",
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# All results are from the right context; semantic score reflects true relevance
|
|
77
|
+
for r in results:
|
|
78
|
+
print(r.metadata["year"], r.metadata["department"], r.score, r.text[:100])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Multi-value filter: search across multiple years
|
|
82
|
+
results = client.search(
|
|
83
|
+
"finance-reports",
|
|
84
|
+
"revenue trend analysis",
|
|
85
|
+
top_k=10,
|
|
86
|
+
filters={
|
|
87
|
+
"year": {"$in": [2023, 2024, 2025]},
|
|
88
|
+
"department": "finance",
|
|
89
|
+
"doc_type": "quarterly-report",
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Filter by recency for support tickets
|
|
94
|
+
recent_tickets = client.search(
|
|
95
|
+
"support-tickets",
|
|
96
|
+
"payment gateway timeout error",
|
|
97
|
+
top_k=8,
|
|
98
|
+
filters={
|
|
99
|
+
"status": "open",
|
|
100
|
+
"created_after": "2026-01-01",
|
|
101
|
+
"priority": {"$in": ["high", "critical"]},
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// TypeScript — hybrid search with metadata filters
|
|
108
|
+
import { Schift } from '@schift-io/sdk';
|
|
109
|
+
|
|
110
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
111
|
+
|
|
112
|
+
// Narrow to 2025 finance docs, then semantic ranking
|
|
113
|
+
const results = await client.search('finance-reports', 'quarterly revenue growth', {
|
|
114
|
+
topK: 5,
|
|
115
|
+
filters: {
|
|
116
|
+
year: 2025,
|
|
117
|
+
department: 'finance',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
results.forEach(r => {
|
|
122
|
+
console.log(r.metadata.year, r.metadata.department, r.score, r.text.slice(0, 100));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Multi-value filter across years
|
|
126
|
+
const trendResults = await client.search('finance-reports', 'revenue trend analysis', {
|
|
127
|
+
topK: 10,
|
|
128
|
+
filters: {
|
|
129
|
+
year: { $in: [2023, 2024, 2025] },
|
|
130
|
+
department: 'finance',
|
|
131
|
+
doc_type: 'quarterly-report',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Filter for recent high-priority support tickets
|
|
136
|
+
const recentTickets = await client.search(
|
|
137
|
+
'support-tickets',
|
|
138
|
+
'payment gateway timeout error',
|
|
139
|
+
{
|
|
140
|
+
topK: 8,
|
|
141
|
+
filters: {
|
|
142
|
+
status: 'open',
|
|
143
|
+
created_after: '2026-01-01',
|
|
144
|
+
priority: { $in: ['high', 'critical'] },
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Supported filter operators:**
|
|
151
|
+
|
|
152
|
+
| Operator | Meaning | Example |
|
|
153
|
+
|----------|---------|---------|
|
|
154
|
+
| exact match | `field: value` | `"year": 2025` |
|
|
155
|
+
| `$in` | any of these values | `"status": { "$in": ["open", "pending"] }` |
|
|
156
|
+
| `$gt` / `$gte` | greater than | `"score": { "$gte": 90 }` |
|
|
157
|
+
| `$lt` / `$lte` | less than | `"created_at": { "$lt": "2025-06-01" }` |
|
|
158
|
+
|
|
159
|
+
Define filterable fields in your collection schema at creation time — only schema-declared fields are indexed for filtering.
|
|
160
|
+
|
|
161
|
+
## Reference
|
|
162
|
+
|
|
163
|
+
- https://docs.schift.io/search/filters
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tune top_k and Similarity Threshold for Your Use Case
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: Using wrong top_k or no score threshold causes either noisy results (too many irrelevant chunks passed to LLM) or missed relevant results, directly degrading answer quality and wasting tokens.
|
|
5
|
+
tags:
|
|
6
|
+
- search
|
|
7
|
+
- retrieval
|
|
8
|
+
- top_k
|
|
9
|
+
- score_threshold
|
|
10
|
+
- precision
|
|
11
|
+
- recall
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Tune top_k and Similarity Threshold for Your Use Case
|
|
15
|
+
|
|
16
|
+
The default `top_k=10` is a reasonable starting point but rarely optimal. Every use case has a different precision/recall trade-off: a chatbot answering factual questions needs tight, high-confidence results, while a research tool benefits from broader recall. Passing too many low-quality chunks to an LLM inflates costs and degrades answer quality; returning too few misses the relevant context entirely.
|
|
17
|
+
|
|
18
|
+
Use `score_threshold` to discard results below a minimum similarity score. Schift returns cosine similarity scores in `[0, 1]` — results below ~0.65 are typically noise.
|
|
19
|
+
|
|
20
|
+
### Incorrect
|
|
21
|
+
|
|
22
|
+
Using default `top_k=10` everywhere with no threshold filter:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# Python — no threshold, fixed top_k for all use cases
|
|
26
|
+
from schift import Schift
|
|
27
|
+
|
|
28
|
+
client = Schift(api_key="sch_...")
|
|
29
|
+
|
|
30
|
+
# Same settings for everything: chatbot QA, exploratory search, code lookup
|
|
31
|
+
results = client.search(collection_id, query) # top_k defaults to 10, no threshold
|
|
32
|
+
|
|
33
|
+
for r in results:
|
|
34
|
+
context += r.text # may include chunks with score=0.42 (largely irrelevant)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// TypeScript — no threshold, fixed top_k for all use cases
|
|
39
|
+
import { Schift } from '@schift-io/sdk';
|
|
40
|
+
|
|
41
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
42
|
+
|
|
43
|
+
// Same settings for everything
|
|
44
|
+
const results = await client.search(collectionId, query); // top_k=10, no threshold
|
|
45
|
+
|
|
46
|
+
const context = results.map(r => r.text).join('\n'); // includes noise
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This passes irrelevant chunks to the LLM, increases prompt token count, and produces hallucinated or unfocused answers.
|
|
50
|
+
|
|
51
|
+
### Correct
|
|
52
|
+
|
|
53
|
+
Set `top_k` and `score_threshold` based on the retrieval goal:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# Python — tuned per use case
|
|
57
|
+
from schift import Schift
|
|
58
|
+
|
|
59
|
+
client = Schift(api_key="sch_...")
|
|
60
|
+
|
|
61
|
+
# Chatbot Q&A: tight precision, high confidence
|
|
62
|
+
qa_results = client.search(
|
|
63
|
+
collection_id,
|
|
64
|
+
query,
|
|
65
|
+
top_k=3, # few but precise chunks
|
|
66
|
+
score_threshold=0.78 # discard anything below 78% similarity
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Research / exploratory: wider recall, lower threshold
|
|
70
|
+
explore_results = client.search(
|
|
71
|
+
collection_id,
|
|
72
|
+
query,
|
|
73
|
+
top_k=30, # broad recall for exploration
|
|
74
|
+
score_threshold=0.55 # accept weaker matches
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Code retrieval: moderate, function-level precision
|
|
78
|
+
code_results = client.search(
|
|
79
|
+
collection_id,
|
|
80
|
+
query,
|
|
81
|
+
top_k=5,
|
|
82
|
+
score_threshold=0.70
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Always check: if nothing passes threshold, surface a "no results" message
|
|
86
|
+
if not qa_results:
|
|
87
|
+
return "I couldn't find relevant information for your question."
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// TypeScript — tuned per use case
|
|
92
|
+
import { Schift } from '@schift-io/sdk';
|
|
93
|
+
|
|
94
|
+
const client = new Schift({ apiKey: 'sch_...' });
|
|
95
|
+
|
|
96
|
+
// Chatbot Q&A: tight precision, high confidence
|
|
97
|
+
const qaResults = await client.search(collectionId, query, {
|
|
98
|
+
topK: 3,
|
|
99
|
+
scoreThreshold: 0.78,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Research / exploratory: wider recall
|
|
103
|
+
const exploreResults = await client.search(collectionId, query, {
|
|
104
|
+
topK: 30,
|
|
105
|
+
scoreThreshold: 0.55,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Code retrieval
|
|
109
|
+
const codeResults = await client.search(collectionId, query, {
|
|
110
|
+
topK: 5,
|
|
111
|
+
scoreThreshold: 0.70,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Handle empty results gracefully
|
|
115
|
+
if (qaResults.length === 0) {
|
|
116
|
+
return "I couldn't find relevant information for your question.";
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Rule of thumb by use case:**
|
|
121
|
+
|
|
122
|
+
| Use Case | top_k | score_threshold |
|
|
123
|
+
|----------|-------|-----------------|
|
|
124
|
+
| Chatbot Q&A | 3–5 | 0.75–0.82 |
|
|
125
|
+
| Document Q&A (long docs) | 5–8 | 0.70–0.78 |
|
|
126
|
+
| Code retrieval | 3–6 | 0.68–0.75 |
|
|
127
|
+
| Research / exploration | 20–50 | 0.50–0.62 |
|
|
128
|
+
| Fact verification | 1–3 | 0.80–0.88 |
|
|
129
|
+
|
|
130
|
+
Start with these ranges and adjust based on observed result quality. Log score distributions during development to calibrate thresholds for your data.
|
|
131
|
+
|
|
132
|
+
## Reference
|
|
133
|
+
|
|
134
|
+
- https://docs.schift.io/search/parameters
|
schift_cli/display.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Sequence
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.progress import (
|
|
8
|
+
BarColumn,
|
|
9
|
+
MofNCompleteColumn,
|
|
10
|
+
Progress,
|
|
11
|
+
SpinnerColumn,
|
|
12
|
+
TextColumn,
|
|
13
|
+
TimeRemainingColumn,
|
|
14
|
+
)
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
error_console = Console(stderr=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# -- Tables -------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
def print_table(
|
|
24
|
+
title: str,
|
|
25
|
+
columns: Sequence[str],
|
|
26
|
+
rows: Sequence[Sequence[Any]],
|
|
27
|
+
*,
|
|
28
|
+
caption: str | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
table = Table(title=title, caption=caption, show_lines=False)
|
|
31
|
+
for col in columns:
|
|
32
|
+
table.add_column(col, overflow="fold")
|
|
33
|
+
for row in rows:
|
|
34
|
+
table.add_row(*(str(v) for v in row))
|
|
35
|
+
console.print(table)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def print_kv(title: str, data: dict[str, Any]) -> None:
|
|
39
|
+
"""Print a key-value panel (e.g. model details, db stats)."""
|
|
40
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
41
|
+
table.add_column("Key", style="bold cyan", no_wrap=True)
|
|
42
|
+
table.add_column("Value")
|
|
43
|
+
for k, v in data.items():
|
|
44
|
+
table.add_row(k, str(v))
|
|
45
|
+
console.print(Panel(table, title=title, border_style="blue"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# -- Status messages ----------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def success(msg: str) -> None:
|
|
51
|
+
console.print(f"[bold green]OK[/] {msg}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def info(msg: str) -> None:
|
|
55
|
+
console.print(f"[bold blue]--[/] {msg}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def warn(msg: str) -> None:
|
|
59
|
+
error_console.print(f"[bold yellow]WARN[/] {msg}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def error(msg: str) -> None:
|
|
63
|
+
error_console.print(f"[bold red]ERROR[/] {msg}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# -- Progress -----------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def make_progress() -> Progress:
|
|
69
|
+
return Progress(
|
|
70
|
+
SpinnerColumn(),
|
|
71
|
+
TextColumn("[progress.description]{task.description}"),
|
|
72
|
+
BarColumn(),
|
|
73
|
+
MofNCompleteColumn(),
|
|
74
|
+
TimeRemainingColumn(),
|
|
75
|
+
console=console,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def spinner(description: str = "Working...") -> Progress:
|
|
80
|
+
"""A simple indeterminate spinner for operations without a known total."""
|
|
81
|
+
return Progress(
|
|
82
|
+
SpinnerColumn(),
|
|
83
|
+
TextColumn("[progress.description]{task.description}"),
|
|
84
|
+
console=console,
|
|
85
|
+
)
|
schift_cli/main.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from schift_cli import __version__
|
|
6
|
+
from schift_cli.commands.auth import auth
|
|
7
|
+
from schift_cli.commands.bench import bench
|
|
8
|
+
from schift_cli.commands.catalog import catalog
|
|
9
|
+
from schift_cli.commands.db import db
|
|
10
|
+
from schift_cli.commands.embed import embed
|
|
11
|
+
from schift_cli.commands.migrate import migrate
|
|
12
|
+
from schift_cli.commands.query import query
|
|
13
|
+
from schift_cli.commands.skill import skill
|
|
14
|
+
from schift_cli.commands.usage import usage
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
@click.version_option(version=__version__, prog_name="schift")
|
|
19
|
+
def cli() -> None:
|
|
20
|
+
"""Schift CLI -- AI Agent Framework.
|
|
21
|
+
|
|
22
|
+
Manage agents, embedding models, vector collections, and migrations
|
|
23
|
+
from the command line.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
cli.add_command(auth)
|
|
28
|
+
cli.add_command(catalog)
|
|
29
|
+
cli.add_command(embed)
|
|
30
|
+
cli.add_command(bench)
|
|
31
|
+
cli.add_command(migrate)
|
|
32
|
+
cli.add_command(db)
|
|
33
|
+
cli.add_command(query)
|
|
34
|
+
cli.add_command(skill)
|
|
35
|
+
cli.add_command(usage)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
cli()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: schift-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Schift CLI — manage agents, embeddings, and vector collections
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: click>=8.1
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: rich>=13.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|