Integrated records (server-side embedding)¶
An index with integrated inference stores text records and embeds them server-side with a hosted model — the same model on write and on read. You upsert text and you search with text; no vector ever passes through your code.
The alternative is to embed the text yourself and upsert vectors. See Generating embeddings for that path, and Upserting and querying vectors for the read side of it.
Create an index with integrated inference¶
create_for_model() is how these indexes are
created. embed needs a model and a field_map:
from pinecone import Pinecone
pc = Pinecone(api_key="your-api-key")
index_model = pc.indexes.create_for_model(
name="semantic-search",
cloud="aws",
region="us-east-1",
embed={"model": "multilingual-e5-large", "field_map": {"text": "chunk_text"}},
)
print(index_model.schema.fields["chunk_text"])
In field_map, the key is the model’s own input field — "text" for the text
models — and the value is the field in your records that holds the content to embed.
The call blocks until the index is ready unless you pass timeout=-1.
EmbedConfig is the typed equivalent of that
dict, and adds read_parameters and write_parameters for the model arguments applied
when embedding a query and an upsert respectively:
from pinecone import EmbedConfig
index_model = pc.indexes.create_for_model(
name="semantic-search",
cloud="aws",
region="us-east-1",
embed=EmbedConfig(
model="multilingual-e5-large",
field_map={"text": "chunk_text"},
write_parameters={"input_type": "passage"},
read_parameters={"input_type": "query"},
),
)
The model cannot be changed after creation.
How this appears in the index schema¶
The embedding configuration comes back as a
SemanticTextField in the index’s schema, named
after the field_map text entry:
index_model = pc.indexes.describe("semantic-search")
print({name: type(f).__name__ for name, f in index_model.schema.fields.items()})
# {'chunk_text': 'SemanticTextField'}
print(index_model.schema.fields["chunk_text"].model)
semantic_text is read-only in a schema. You cannot declare one in a schema= you pass
to create() — sending it is rejected, and a single
rejected field fails the whole schema. create_for_model is what produces the field.
create(schema=...) is for the index shapes where you supply the vectors yourself:
dense_vector, sparse_vector, and string with full-text search. See
Working with serverless indexes.
Passing spec=IntegratedSpec(...) to create() raises
PineconeTypeError; the message prints the equivalent
create_for_model call, filled in from the spec you passed.
Get a handle to the index¶
index = pc.index("semantic-search")
Upsert records¶
upsert_records() takes record dicts, each with an _id (or
id) plus the field named in field_map. Records are sent as newline-delimited JSON
and embedded server-side:
response = index.upsert_records(
namespace="en",
records=[
{"_id": "article-1", "chunk_text": "Vector databases accelerate AI search."},
{"_id": "article-2", "chunk_text": "RAG pipelines combine retrieval with generation."},
{"_id": "article-3", "chunk_text": "Pinecone scales to billions of vectors."},
],
)
print(response.record_count)
namespace is required and must be non-empty. Unlike
upsert(), this API has no default namespace to fall back on,
and the SDK rejects the call before sending anything.
Any other keys on the record are stored as metadata and indexed for filtering
automatically — they need no schema declaration. A record giving both _id and id
keeps _id. A missing or non-string _id raises
PineconeValueError, naming the offending record’s
position, before any HTTP request.
record_count is the number of records you submitted, counted client-side. Writes are not
readable the instant this returns, so allow for freshness lag before searching for a
record you just wrote.
Search records¶
search() with inputs sends text, and the index’s own model
embeds it as the query vector:
results = index.search(
namespace="en",
top_k=5,
inputs={"text": "AI and machine learning"},
)
for hit in results.result.hits:
print(hit.id, hit.score, hit.fields)
The "text" key here is the model’s input field, the same key as on the left of
field_map — not the name of your record field.
search also accepts vector= or id= in place of inputs=, for when you already hold
a vector or want an existing record to serve as the query. Exactly one query source is
required.
Response: hits, not matches¶
search returns a SearchRecordsResponse, and
its shape is not query()’s:
|
|
|
|---|---|---|
Where the results live |
|
|
One result |
||
Your record’s data |
|
split across |
The extra result step is the response envelope, and forgetting it is the usual first
stumble. A search that matched nothing returns an empty hits list rather than raising.
Each Hit exposes:
.id— the record identifier..score— how well the record matched, higher being better. After reranking the scale is the reranker’s, not the index’s, so compare scores only within one response..fields— your record’s own fields, keyed by field name, subject to thefieldsargument below.
.usage breaks the cost out by stage: read_units always, embed_total_tokens only when
the index embedded your text, and rerank_units only when you passed rerank. A stage
that did not run reports None.
For the vector-supplied side of the comparison, see query or search: pick
one.
Typed dicts for inputs and rerank¶
SearchInputs and
RerankConfig are
TypedDict definitions, so there is nothing to instantiate — annotate a
plain dict and your editor and type checker will check the keys for you:
from pinecone import SearchInputs
inputs: SearchInputs = {"text": "AI and machine learning"}
results = index.search(namespace="en", top_k=5, inputs=inputs)
Rerank in a single search call¶
A rerank config retrieves and reranks in one request. model and rank_fields are
both required, and the SDK rejects the call before sending if either is missing:
results = index.search(
namespace="en",
top_k=50,
inputs={"text": "best practices for vector search"},
rerank={
"model": "bge-reranker-v2-m3",
"rank_fields": ["chunk_text"],
"top_n": 5,
},
)
for hit in results.result.hits:
print(hit.id, hit.score)
top_k is how many candidates the search retrieves; top_n is how many survive
reranking, defaulting to top_k. Set top_n lower to have the reranker narrow a wider
candidate set — that is the whole reason to retrieve more than you intend to show.
rank_fields names record fields, so they must be fields the search returns. Add
query to the rerank config when the text to rerank against should differ from the search
text.
For reranking candidates that came from somewhere other than this index, use
pc.inference.rerank. See Reranking results.
Filter by metadata¶
results = index.search(
namespace="en",
top_k=5,
inputs={"text": "quantum computing"},
filter={"category": {"$eq": "science"}},
)
print(len(results.result.hits))
The filter grammar is the same one query() uses — see
Filter by metadata.
Select returned fields¶
Omitting fields returns every field each record has. Narrow it when you only need one
or two:
results = index.search(
namespace="en",
top_k=5,
inputs={"text": "AI research"},
fields=["chunk_text", "category"],
)
See also¶
Generating embeddings — embed text yourself, for an index you supply vectors to.
Reranking results — rerank candidates from any source.
Upserting and querying vectors — the vector-in, vector-out surface.