Skip to content

Commit 9be28be

Browse files
authored
Merge pull request #21 from pinecone-io/sync/skills
sync: skills from pinecone-io/skills: FTS update
2 parents 53134c8 + f42c43d commit 9be28be

16 files changed

Lines changed: 350 additions & 212 deletions

File tree

-6.06 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
-7.73 KB
Binary file not shown.
-15.8 KB
Binary file not shown.
Binary file not shown.

skills/pinecone-full-text-search/SKILL.md

Lines changed: 115 additions & 70 deletions
Large diffs are not rendered by default.

skills/pinecone-full-text-search/references/ingestion.md

Lines changed: 78 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Ingestion
22

3-
Writing documents into a Pinecone preview document index uses two methods. Pick based on volume, then handle the *async indexing* gotcha on the other side.
3+
Writing documents into a Pinecone document index uses two methods. Pick based on volume, then handle the *async indexing* gotcha on the other side.
44

55
## `documents.upsert` — small writes / patches
66

77
```python
8-
idx = pc.preview.index(name=INDEX_NAME)
8+
idx = pc.index(name=INDEX_NAME)
99

1010
upsert_resp = idx.documents.upsert(
1111
namespace=NAMESPACE,
@@ -14,10 +14,10 @@ upsert_resp = idx.documents.upsert(
1414
"_id": "doc-1",
1515
"title": "A landmark work that every reader should experience.",
1616
"body": "Lorem ipsum...",
17-
"category": "fiction",
17+
"category": "fiction", # not declared in schema — auto-indexed for filtering anyway
1818
"year": 2024.0,
1919
},
20-
# ... up to ~1000 documents per call (per public-preview docs)
20+
# ... up to ~1000 documents per call
2121
],
2222
)
2323
print(upsert_resp.upserted_count)
@@ -26,10 +26,10 @@ print(upsert_resp.upserted_count)
2626
Use `upsert` when:
2727

2828
- You're writing a single document (e.g. a sentinel doc to verify end-to-end before a bulk load).
29-
- You're "patching" a doc after a correction. *Note*: `2026-01.alpha` has **no per-field merge** — every upsert replaces the entire document on conflicting `_id`. To update a single field, fetch the doc, modify in client code, and upsert the full doc back under the same `_id`.
3029
- You're streaming writes from user actions and each request fits in a single batch.
30+
- You want to fully replace a document by `_id` (upsert always replaces the whole document on conflict — for a true per-field patch, use `documents.update` below instead).
3131

32-
Each document is a dict keyed by field name. `_id` is required and must be a non-empty unique string within the namespace. Values must match the declared schema types (FTS strings → `str`, filterable `float``int|float`, dense vectors → `list[float]`, sparse → `{"indices": [...], "values": [...]}`). Field names that start with `_` or `$` are rejected; field names are limited to 64 bytes.
32+
Each document is a dict keyed by field name (or a `DocumentRecord`/`UpdateDocumentRecord` instance — both accept a plain dict positionally or fields as keyword arguments). `_id` is required and must be a non-empty unique string within the namespace. Values must match the declared schema types for the few fields actually declared in the schema (FTS strings → `str`, dense vectors → `list[float]`, sparse → `{"indices": [...], "values": [...]}`); every other field on the document — strings, numbers, booleans, string lists — is auto-indexed metadata with nothing declared for it. Field names that start with `_` or `$` are rejected; field names are limited to 64 bytes.
3333

3434
The endpoint returns `202 Accepted` (async) and the body's `upserted_count` is the number of items accepted, not the number that have finished indexing.
3535

@@ -39,8 +39,8 @@ The endpoint returns `202 Accepted` (async) and the body's `upserted_count` is t
3939
result = idx.documents.batch_upsert(
4040
namespace=NAMESPACE,
4141
documents=documents, # list of dicts, any length
42-
batch_size=50,
43-
max_workers=2,
42+
batch_size=50, # SDK default
43+
max_concurrency=4, # SDK default
4444
show_progress=True,
4545
)
4646
print(f"{result.successful_item_count:,} / {result.total_item_count:,} succeeded")
@@ -54,17 +54,17 @@ if result.has_errors:
5454
f"first _id={sample!r}): {err.error_message}")
5555
```
5656

57-
The SDK splits `documents` into `batch_size`-sized chunks and uploads them over `max_workers` parallel HTTP connections. `show_progress=True` prints a tqdm-style bar.
57+
The SDK splits `documents` into `batch_size`-sized chunks and uploads them over `max_concurrency` parallel HTTP connections. `show_progress=True` prints a tqdm-style bar. `max_concurrency` must be a plain `int` (default `4`) — it no longer accepts `None`.
5858

59-
### Tuning `batch_size` and `max_workers`
59+
### Tuning `batch_size` and `max_concurrency`
6060

61-
- **`batch_size=50`** is the sweet spot — comfortably below the per-request cap and small enough that transient failures cost less to redo.
62-
- **`max_workers=2`** is a safe default. Bump to `4` for large (thousands-of-docs) loads where you're not simultaneously embedding. Ramp cautiously above 4 — you'll hit Pinecone or upstream embedding-provider rate limits first.
63-
- If you're embedding on the fly (computing vectors inside the upsert loop), keep `max_workers` low so embedding latency dominates rather than index write latency.
61+
- **`batch_size=50`** (the SDK's own default) is the sweet spot — comfortably below the per-request cap and small enough that transient failures cost less to redo.
62+
- **`max_concurrency=4`** (the SDK's own default) is a safe default for large (thousands-of-docs) loads where you're not simultaneously embedding. Ramp cautiously above 4 — you'll hit Pinecone or upstream embedding-provider rate limits first.
63+
- If you're embedding on the fly (computing vectors inside the upsert loop), keep `max_concurrency` low so embedding latency dominates rather than index write latency.
6464

6565
### Document and request size caps
6666

67-
**Hard limits in `2026-01.alpha`:**
67+
**Hard limits in `2026-07`:**
6868

6969
- **Per document**: max **2 MB** (serialized JSON, all stored fields combined).
7070
- **Per `full_text_search` string field**: max **100 KB** AND max **10,000 tokens**. Tokens longer than 256 bytes are silently truncated by the analyzer.
@@ -76,7 +76,7 @@ If any one of these is exceeded, the batch fails as a whole. The most common lim
7676

7777
### Dense-vector payload size
7878

79-
A high-dimensional dense field can silently turn a 50-doc batch into a 5–10 MB request, which the preview backend will reject wholesale. If every batch fails and the error message is opaque, the first thing to try is dropping the embedding dimension before debugging schema:
79+
A high-dimensional dense field can silently turn a 50-doc batch into a 5–10 MB request, which the backend will reject wholesale. If every batch fails and the error message is opaque, the first thing to try is dropping the embedding dimension before debugging schema:
8080

8181
- **Gemini**: pass `config=types.EmbedContentConfig(output_dimensionality=768)`. The model uses Matryoshka representations, so smaller dimensions are valid truncations of the native output. 768 is usually a 4× payload reduction vs. the native 3072 and costs very little quality.
8282
- **OpenAI `text-embedding-3-*`**: pass `dimensions=768` (or similar) to `embeddings.create`.
@@ -86,6 +86,8 @@ A high-dimensional dense field can silently turn a 50-doc batch into a 5–10 MB
8686

8787
After `batch_upsert` returns, **your documents are written but not yet searchable.** The server builds inverted indexes for FTS fields and ANN graphs for vector fields in the background. A search query issued immediately will return empty matches. Schemas with multiple indexed fields (e.g. text + dense + sparse) may take slightly longer.
8888

89+
This is distinct from — and in addition to — `pc.indexes.create(...)`'s own polling for the *index itself* to become ready (which it does by default before returning). Even a fully-ready index needs this separate poll after each ingest.
90+
8991
**Always poll with a deadline** before trusting the index:
9092

9193
```python
@@ -111,7 +113,7 @@ Pick a sentinel query likely to hit at least one document. For a typical corpus,
111113

112114
## Chunking oversized text
113115

114-
Per the public-preview docs (above), the per-FTS-field hard limits are 100 KB and 10,000 tokens. In practice, plan for the *token* limit kicking in first on natural prose (~5,000 English words at ~2 tokens each is the rough ceiling). Probe before ingesting at scale — chunk anything that approaches either bound, with safety margin.
116+
Per the size caps above, the per-FTS-field hard limits are 100 KB and 10,000 tokens. In practice, plan for the *token* limit kicking in first on natural prose (~5,000 English words at ~2 tokens each is the rough ceiling). Probe before ingesting at scale — chunk anything that approaches either bound, with safety margin.
115117

116118
**Strategy: probe first, then chunk if needed.**
117119

@@ -158,40 +160,80 @@ Conventions:
158160

159161
## Updating documents
160162

161-
There is **no per-field update or merge** in `2026-01.alpha`. `documents.upsert` always replaces the entire document for a given `_id`. To update one field:
163+
`2026-07` has real per-field updates via `documents.update(...)` — this replaces the fetch → modify → re-upsert workaround from the old preview API. Two shapes:
164+
165+
**By `_id`, patching specific fields:**
162166

163167
```python
164-
fetched = idx.documents.fetch(
168+
idx.documents.update(
165169
namespace=NAMESPACE,
166-
ids=["doc-42"],
167-
include_fields=["*"], # need the full doc to round-trip it
170+
documents=[{"_id": "doc-42", "set_fields": {"category": "biography"}}],
168171
)
169-
doc = fetched.documents["doc-42"].to_dict()
170-
doc["category"] = "biography" # patch in client code
172+
```
171173

172-
idx.documents.upsert(namespace=NAMESPACE, documents=[doc])
174+
**By filter, patching every matching document at once:**
175+
176+
```python
177+
resp = idx.documents.update(
178+
namespace=NAMESPACE,
179+
filter={"category": {"$eq": "fiction"}},
180+
set_fields={"featured": True},
181+
# remove_fields=["stale_field"], # drop a field entirely, by name
182+
)
183+
print(resp.matched_records) # point-in-time count, same semantics as documents.delete
173184
```
174185

175-
If the document includes a dense vector, you re-upsert that vector verbatim. If it changes, embed the new content first.
186+
`documents.update` accepts `documents=` (a list of per-record patches) *or* `filter=` + `set_fields=`/`remove_fields=` (a bulk patch by metadata match) — not both. `set_fields` merges into the existing document; fields you don't mention are left untouched. `remove_fields` deletes named fields outright. For a filtered update, `matched_records` on the response is a point-in-time count when the server accepted the request, the same caveat as `documents.delete`'s `matched_records` — the update itself applies asynchronously.
187+
188+
If you need to fully replace a document (including its vector fields) rather than patch it, `documents.upsert` with the complete document under the same `_id` is still the right tool — `upsert` replaces, `update` patches.
176189

177190
## Deletes
178191

179-
`documents.delete` accepts either `ids: [...]` (1–1000 items) or `delete_all: true`. There is **no delete-by-filter** — to delete documents matching a metadata expression, search first to collect IDs, then pass them in:
192+
`documents.delete` accepts exactly one of `ids: [...]` (1–1000 items), `filter: {...}`, or `delete_all: true`:
180193

181194
```python
182-
ids_to_kill = [
183-
m._id for m in idx.documents.search(
184-
namespace=NAMESPACE, top_k=1000,
185-
score_by=[{"type": "text", "field": "body", "query": "deprecated"}],
186-
filter={"category": {"$eq": "archive"}},
187-
include_fields=[],
188-
).matches
189-
]
190-
idx.documents.delete(namespace=NAMESPACE, ids=ids_to_kill)
195+
# Delete-by-filter — no need to search for IDs first anymore.
196+
resp = idx.documents.delete(namespace=NAMESPACE, filter={"category": {"$eq": "archive"}})
197+
print(resp.matched_records)
191198
```
192199

193200
`delete_all=True` wipes the entire namespace. Use carefully.
194201

202+
## Namespace management
203+
204+
Confirmed working against document-schema indexes in `2026-07` — this was **not** supported in the old preview API, which could write to a namespace but not list, describe, create, or delete one via the API.
205+
206+
```python
207+
# Create explicitly (namespaces otherwise auto-create on first upsert — see below).
208+
ns = idx.create_namespace(name="movies-en")
209+
print(ns.name, ns.record_count, ns.size_bytes)
210+
211+
# List every namespace on the index in one call.
212+
for page in idx.list_namespaces():
213+
for ns in page.namespaces:
214+
print(ns.name, ns.record_count)
215+
216+
# Describe one namespace. Prefer list_namespaces() over repeated describe_namespace()
217+
# calls — describe_namespace is rate-limited per index; list_namespaces isn't.
218+
ns = idx.describe_namespace(name="movies-en")
219+
220+
# Delete a namespace and everything in it.
221+
idx.delete_namespace(name="movies-en")
222+
```
223+
224+
`create_namespace`'s optional `schema` parameter (`{"fields": {"<field>": {"filterable": True}}}`) controls *which metadata fields get indexed for filtering in that namespace specifically* — omitting it means the namespace inherits the index's own metadata-indexing configuration, which for the managed indexes this skill covers is "index everything" by default (see "Filterable metadata isn't declared in the schema at all" in SKILL.md). There's rarely a reason to pass it explicitly unless you're deliberately restricting which fields are filterable in one namespace.
225+
226+
`__default__` is reserved — it always exists and can't be created or deleted; every namespace-taking call already defaults to it when `namespace` is omitted, but it's worth knowing when you see it show up unbidden in a `list_namespaces()` result.
227+
228+
### `describe_index_stats` — also now works
229+
230+
```python
231+
stats = idx.describe_index_stats()
232+
print(stats.total_vector_count, stats.namespaces) # total record count, namespace count
233+
```
234+
235+
`describe_index_stats(filter=...)` is documented as rejected on every index type (there's no operation that returns a filtered count) — call it with no arguments.
236+
195237
## Integrating embedding providers
196238

197239
If your index has a dense or sparse vector field, you need embeddings. Three common paths:
@@ -256,8 +298,7 @@ This adapter also gives you a single chokepoint for retries, rate-limit backoff,
256298

257299
## Limits to be aware of
258300

259-
- **No bulk import (S3 import job)** for document-shaped indexes in `2026-01.alpha`. Load through `documents.upsert` / `documents.batch_upsert`.
301+
- **No bulk import (S3 import job)** for document-shaped indexes in `2026-07`. Load through `documents.upsert` / `documents.batch_upsert`.
260302
- **No backup/restore.** If you need recoverability, snapshot your source data, not the index.
261-
- **No CMEK projects** — indexes can't be created in CMEK-enabled projects.
303+
- **No CMEK projects alongside any `full_text_search` field** such indexes can't be created in CMEK-enabled projects.
262304
- **Indexing latency**: documents become searchable in ≲1 minute typically; multi-field schemas can take slightly longer.
263-

0 commit comments

Comments
 (0)