Searching
Everything on this page is a method on the BuiltPipeline that
cg.build() returns. None of it needs an eval set — run_queries() takes one, but search()
alone is enough to try a pipeline out.
import contextgrid as cg
corpus = cg.Corpus.from_texts({ "refunds.md": "refunds take within 30 days of purchase. digital goods are not refundable once downloaded.", "shipping.md": "express shipping arrives the next business day.",})config = cg.Config(parser="markdown", chunker="recursive:512", embedder="tfidf", index="dense", k=2)pipeline = cg.build(config, corpus)search()
pipeline.search(query: str, k: int | None = None) -> list[str]Takes a question, returns a list of chunk ids, best match first. k overrides
config.k for this one call; leave it off and config.k is used.
ids = pipeline.search("How long do refunds take?")print(ids)['refunds.md:0-90', 'shipping.md:0-47']That’s it — a chunk id, not a Chunk. To get the text back, see chunk_by_id() below.
With config.reranker set, search() asks the retriever for config.candidates results
first and lets the reranker cut that down to k; without one, the retriever is asked for k
directly, so a no-reranker pipeline never pays for candidates it would throw away. Which
retrieval strategy actually gathers those candidates — one lookup, several fused, a model
deciding as it goes — is set by config.retrieval; see Retrieval.
scored_ids()
pipeline.scored_ids(returned: Sequence[str]) -> list[str]Takes the chunk ids search() returned and maps them onto the ids your eval set’s ground
truth actually uses, in order, without repeats. For a plain configuration (no
ingestion strategy reshaping what gets indexed vs. what gets returned),
this is the identity — you get the same ids back:
scored = pipeline.scored_ids(ids)print(scored)['refunds.md:0-90', 'shipping.md:0-47']It stops being the identity once config.ingestion is something like contextual or
propositions, where several indexed units can stand for one presented passage — that’s the
whole reason this method exists, and why scoring should always go through it rather than
scoring search()’s output directly.
run_queries()
pipeline.run_queries(evalset: EvalSet, k: int | None = None) -> dict[str, list[str]]Runs search() (then scored_ids()) for every item in an EvalSet, keyed by
item.id. It also records each query’s wall-clock time into
pipeline.timings.query_ms — build() never touches that list, so before the first call it
is empty.
evalset = cg.EvalSet( id="demo", items=( cg.EvalItem(id="q1", question="How long do refunds take?"), cg.EvalItem(id="q2", question="How fast is express shipping?"), ),)
run = pipeline.run_queries(evalset)print(run)print(pipeline.timings.query_ms){'q1': ['refunds.md:0-90', 'shipping.md:0-47'], 'q2': ['shipping.md:0-47', 'refunds.md:0-90']}[0.048..., 0.554...]The result is a Run (a plain dict[str, list[str]]) in the shape every metric in
Scoring expects — pass it alongside a Qrels to cg.evaluate().
Loading or building an EvalSet in the first place is covered in
Loading an Eval Set.
chunk_by_id()
pipeline.chunk_by_id() -> dict[str, Chunk]Every chunk that can come back from a search, keyed by id — including presentation passages
from an ingestion strategy, which is why reranking and generation both use this map rather
than indexing into pipeline.chunks directly.
by_id = pipeline.chunk_by_id()print(list(by_id.keys()))print(by_id["refunds.md:0-90"].text)['refunds.md:0-90', 'shipping.md:0-47']refunds take within 30 days of purchase. digital goods are not refundable once downloaded.Rebuilt on every call — it’s a dict comprehension over pipeline.chunks plus
pipeline.ingested.presented_chunks, not cached. Fine for occasional lookups; call it once
and reuse the result if you’re looking up many ids in a loop.
Next: Generating an Answer covers turning a search() result into text
with pipeline.answer().