"""The four ways a document search can score a document, and the union over them.
Every ``score_by`` clause is one of these four, told apart by its ``type`` tag:
``text`` for BM25 keyword scoring, ``query_string`` for a Lucene expression,
``dense_vector`` for similarity to an embedding, and ``sparse_vector`` for similarity to
a sparse vector. Pass them as typed instances or as plain dicts with a ``type`` key —
either way each one checks its required fields on construction, so a malformed clause is
rejected at the call site.
``text`` and ``query_string`` clauses can be combined in one search to score on several
signals at once. A vector clause has to stand alone.
"""
from __future__ import annotations
import warnings
from msgspec import Struct
from pinecone.models.vectors.sparse import SparseValues
__all__ = [
"DenseVectorQuery",
"DocumentScoringMethod",
"QueryStringQuery",
"SparseVectorQuery",
"TextQuery",
]
[docs]
class TextQuery(Struct, tag="text", tag_field="type", kw_only=True, omit_defaults=True):
"""Score documents by BM25 keyword relevance over named text fields.
The clause to reach for when the words themselves matter — exact terms, names, codes
— rather than meaning. Scoring is per field, so name every field you want searched.
Attributes:
query: The words to search for, e.g. ``"sparse index scoring"``.
fields: The text fields to search across, e.g. ``["title", "chunk"]``. At least
one is required.
field: Deprecated single-field form of ``fields``. A value here is moved into
``fields`` with a :exc:`DeprecationWarning` and this attribute reads back as
``None``, so read ``fields`` whichever way you set it.
Raises:
ValueError: If ``fields`` ends up empty, or if both ``fields`` and ``field`` are
given.
Examples:
>>> from pinecone.models.documents.score_by import TextQuery
>>> TextQuery(query="sparse index scoring", fields=["title", "chunk"]).fields
['title', 'chunk']
.. seealso::
:class:`QueryStringQuery` — when you need boolean operators in the query itself.
"""
query: str
fields: list[str] | None = None
field: str | None = None
def __post_init__(self) -> None:
if self.field is not None:
if self.fields is not None:
raise ValueError(
"TextQuery accepts `fields=[...]` or the deprecated `field=...`, but not both."
)
warnings.warn(
"TextQuery `field` is deprecated; use `fields=[...]`.",
DeprecationWarning,
stacklevel=2,
)
self.fields = [self.field]
self.field = None
if not self.fields:
raise ValueError("Text scoring requires specifying at least one field")
[docs]
class QueryStringQuery(
Struct, tag="query_string", tag_field="type", kw_only=True, omit_defaults=True
):
"""Score documents by a Lucene query string, with ``AND``, ``OR`` and ``NOT``.
Choose this over :class:`TextQuery` when the query itself needs structure — combining
terms, excluding one, or scoping a clause to one field. Fields are named *inside* the
query string as ``field_name:(clause)``; leave the qualifiers off and every
text-searchable field is searched.
Attributes:
query: The Lucene expression, e.g. ``'title:(sparse OR hybrid) NOT draft'``.
field: Not accepted here — use a qualifier in ``query`` instead.
fields: Not accepted here — use a qualifier in ``query`` instead.
Raises:
ValueError: If ``field`` or ``fields`` is given. Passing either is rejected rather
than ignored, because the field would silently not be applied.
Examples:
>>> from pinecone.models.documents.score_by import QueryStringQuery
>>> QueryStringQuery(query="title:(sparse OR hybrid) NOT draft").query
'title:(sparse OR hybrid) NOT draft'
"""
query: str
field: str | None = None
fields: list[str] | None = None
def __post_init__(self) -> None:
if self.field is not None or self.fields is not None:
raise ValueError(
"'score_by' clauses of type 'query_string' must not specify 'field' or "
"'fields'; use Lucene-style field qualifiers (e.g. 'field_name:value') "
"in the query string instead"
)
[docs]
class DenseVectorQuery(Struct, tag="dense_vector", tag_field="type", kw_only=True):
"""Score documents by dense vector similarity to a query embedding.
The clause for finding documents by meaning. It scores against one field, so the
field named has to be declared ``dense_vector`` in the index schema, and ``values``
has to be as long as that field's ``dimension``. A dense clause cannot be combined
with any other scoring method in the same search.
Attributes:
field: The dense vector field to score against, e.g. ``"embedding"``.
values: The query embedding, one float per dimension of that field.
Raises:
ValueError: If ``field`` is empty, or ``values`` is empty.
Examples:
The three floats here stand in for a full-length embedding.
>>> from pinecone.models.documents.score_by import DenseVectorQuery
>>> DenseVectorQuery(field="embedding", values=[0.12, 0.34, 0.56]).field
'embedding'
.. seealso::
:class:`SparseVectorQuery` — the same idea against a sparse field.
"""
field: str
values: list[float]
def __post_init__(self) -> None:
if not self.field:
raise ValueError(
"dense_vector scoring requires specifying the name of exactly one field"
)
if not self.values:
raise ValueError("dense_vector scoring requires a non-empty 'values' array")
[docs]
class SparseVectorQuery(Struct, tag="sparse_vector", tag_field="type", kw_only=True):
"""Score documents by sparse vector similarity to a query sparse vector.
The clause for finding documents by term overlap when you already hold a sparse
encoding of the query. The field named has to be declared ``sparse_vector`` in the
index schema, and, like a dense clause, this one cannot be combined with any other
scoring method in the same search.
Attributes:
field: The sparse vector field to score against, e.g. ``"keywords"``.
sparse_values: The query's sparse vector, as
:class:`~pinecone.models.vectors.sparse.SparseValues`.
Raises:
ValueError: If ``field`` is empty.
Examples:
>>> from pinecone import SparseValues
>>> from pinecone.models.documents.score_by import SparseVectorQuery
>>> SparseVectorQuery(
... field="keywords",
... sparse_values=SparseValues(indices=[10, 42], values=[0.4, 0.9]),
... ).field
'keywords'
.. seealso::
:class:`TextQuery` — when you have the words rather than a sparse encoding of them.
"""
field: str
sparse_values: SparseValues
def __post_init__(self) -> None:
if not self.field:
raise ValueError(
"sparse_vector scoring requires specifying the name of exactly one field"
)
DocumentScoringMethod = TextQuery | QueryStringQuery | DenseVectorQuery | SparseVectorQuery