跳转至

Planner#

nlql.plan —— 查询规划:相关度提取(Scorer / score_key)、QueryPlan,以及过滤操作的拆分(split_filter / is_pushable),决定哪些操作可交给后端、哪些在内存执行。

plan #

Query planning: analysis, scoring extraction, and (future) pushdown splitting.

__all__ module-attribute #

__all__ = ['Planner', 'QueryPlan', 'Scorer', 'score_key', 'FilterSplit', 'split_filter', 'is_pushable', 'metadata_field']

QueryPlan dataclass #

QueryPlan(query: Query, scorers: list[Scorer] = list(), bindings: dict[str, Expr] = dict(), granularity: str = 'sentence', pushed_filter: Expr | None = None, residual_filter: Expr | None = None, store: str = 'local')

The analyzed form of a query, ready for the executor.

query instance-attribute #

query: Query

scorers class-attribute instance-attribute #

scorers: list[Scorer] = field(default_factory=list)

bindings class-attribute instance-attribute #

bindings: dict[str, Expr] = field(default_factory=dict)

granularity class-attribute instance-attribute #

granularity: str = 'sentence'

pushed_filter class-attribute instance-attribute #

pushed_filter: Expr | None = None

residual_filter class-attribute instance-attribute #

residual_filter: Expr | None = None

store class-attribute instance-attribute #

store: str = 'local'

explain #

explain() -> dict[str, Any]

A JSON-friendly description of the plan for EXPLAIN.

源代码位于: src/nlql/plan/plan.py
def explain(self) -> dict[str, Any]:
    """A JSON-friendly description of the plan for EXPLAIN."""
    select = self.query.select
    return {
        "select": {"unit": select.unit, "window": select.window},
        "granularity": self.granularity,
        "store": self.store,
        "scores": [
            {"key_alias": self._alias_for(s), "query": s.query_text, "path": s.path}
            for s in self.scorers
        ],
        "bindings": list(self.bindings),
        "filter": {
            "pushed": self.pushed_filter.to_dict() if self.pushed_filter is not None else None,
            "residual": (
                self.residual_filter.to_dict() if self.residual_filter is not None else None
            ),
        },
        "order_by": [
            {"expr": k.expr.to_dict(), "desc": k.desc} for k in self.query.order_by
        ],
        "limit": self.query.limit,
        "recall": "exact-flat" if self.scorers else "scan",
    }

Scorer dataclass #

Scorer(key: str, query_text: str, path: str, call: Call, vector_name: str = 'default')

A semantic score to compute during recall.

key instance-attribute #

key: str

query_text instance-attribute #

query_text: str

path instance-attribute #

path: str

call instance-attribute #

call: Call

vector_name class-attribute instance-attribute #

vector_name: str = 'default'

Planner #

Planner(registry: Registry)

Turns a :class:Query into a :class:QueryPlan.

源代码位于: src/nlql/plan/planner.py
def __init__(self, registry: Registry) -> None:
    self._registry = registry

plan #

plan(query: Query, *, granularity: str, caps: StoreCaps | None = None, field_types: dict[str, Any] | None = None) -> QueryPlan
源代码位于: src/nlql/plan/planner.py
def plan(
    self,
    query: Query,
    *,
    granularity: str,
    caps: StoreCaps | None = None,
    field_types: dict[str, Any] | None = None,
) -> QueryPlan:
    caps = caps or StoreCaps()
    bindings: dict[str, Expr] = {b.name: b.expr for b in query.let}
    scorers: dict[str, Scorer] = {}
    visiting: set[str] = set()

    def visit(expr: Expr) -> None:
        if isinstance(expr, Call):
            cap = self._registry.get("function", expr.name)
            if cap is None:
                raise NLQLPlanError(f"unknown function {expr.name!r}")
            if cap.signature is not None and not cap.signature.arity_ok(len(expr.args)):
                raise NLQLPlanError(
                    f"{expr.name} expects {len(cap.signature.args)} args, got {len(expr.args)}"
                )
            if cap.provides_score:
                self._register_scorer(expr, scorers)
            for arg in expr.args:
                visit(arg)
        elif isinstance(expr, Compare):
            visit(expr.left)
            visit(expr.right)
        elif isinstance(expr, (And, Or)):
            for operand in expr.operands:
                visit(operand)
        elif isinstance(expr, Not):
            visit(expr.operand)
        elif isinstance(expr, Ref):
            if expr.name not in bindings:
                raise NLQLPlanError(f"unknown alias {expr.name!r} (no matching LET binding)")
            if expr.name not in visiting:  # guard against cyclic bindings
                visiting.add(expr.name)
                visit(bindings[expr.name])
                visiting.discard(expr.name)
        # Literal / Path: nothing to analyze

    for binding in query.let:
        visit(binding.expr)
    if query.where is not None:
        visit(query.where)
    for key in query.order_by:
        visit(key.expr)

    split = split_filter(query.where, caps, field_types)
    return QueryPlan(
        query=query,
        scorers=list(scorers.values()),
        bindings=bindings,
        granularity=granularity,
        pushed_filter=split.pushed,
        residual_filter=split.residual,
        store=caps.name,
    )

FilterSplit dataclass #

FilterSplit(pushed: Expr | None, residual: Expr | None)

A WHERE clause partitioned into store-native and in-memory parts.

pushed instance-attribute #

pushed: Expr | None

residual instance-attribute #

residual: Expr | None

score_key #

score_key(call: Call) -> str

Canonical, stable identity of a scoring call (e.g. a SIMILARITY invocation).

源代码位于: src/nlql/plan/plan.py
def score_key(call: Call) -> str:
    """Canonical, stable identity of a scoring call (e.g. a SIMILARITY invocation)."""
    return json.dumps(call.to_dict(), sort_keys=True, ensure_ascii=False)

is_pushable #

is_pushable(expr: Expr, caps: StoreCaps, field_types: dict[str, Any] | None = None) -> bool

Whether expr can be pushed to a store with the given capabilities.

源代码位于: src/nlql/plan/pushdown.py
def is_pushable(
    expr: Expr, caps: StoreCaps, field_types: dict[str, Any] | None = None
) -> bool:
    """Whether ``expr`` can be pushed to a store with the given capabilities."""
    return _is_pushable_subtree(expr, caps, field_types or {})

metadata_field #

metadata_field(path: Path) -> str

The dotted metadata key for a path (drops the optional meta prefix).

源代码位于: src/nlql/plan/pushdown.py
def metadata_field(path: Path) -> str:
    """The dotted metadata key for a path (drops the optional ``meta`` prefix)."""
    segments = path.segments if path.root == "meta" else [path.root, *path.segments]
    return ".".join(segments)

split_filter #

split_filter(where: Expr | None, caps: StoreCaps, field_types: dict[str, Any] | None = None) -> FilterSplit

Partition a WHERE clause into (pushed, residual) given store capabilities.

源代码位于: src/nlql/plan/pushdown.py
def split_filter(
    where: Expr | None, caps: StoreCaps, field_types: dict[str, Any] | None = None
) -> FilterSplit:
    """Partition a WHERE clause into (pushed, residual) given store capabilities."""
    if where is None:
        return FilterSplit(None, None)
    types = field_types or {}
    conjuncts = list(where.operands) if isinstance(where, And) else [where]
    pushed: list[Expr] = []
    residual: list[Expr] = []
    for conjunct in conjuncts:
        (pushed if is_pushable(conjunct, caps, types) else residual).append(conjunct)
    return FilterSplit(_combine(pushed), _combine(residual))