{"record":{"id":"022c83ff5d53f002","repo":"microsoft/semantic-kernel","slug":"unsupported-attribute-chain-root-type-current","errorCode":null,"errorMessage":"Unsupported attribute chain root: {type(current)}","messagePattern":"Unsupported attribute chain root: (.+?)","errorType":"exception","errorClass":"NotImplementedError","httpStatus":null,"severity":"error","filePath":"python/semantic_kernel/connectors/azure_ai_search.py","lineNumber":647,"sourceCode":"            raise VectorSearchExecutionException(\"Failed to search the collection.\") from exc\n        return KernelSearchResults(\n            results=self._get_vector_search_results_from_results(raw_results, options),\n            total_count=await raw_results.get_count() if options.include_total_count else None,\n        )\n\n    @override\n    def _lambda_parser(self, node: ast.AST) -> Any:\n        def _parse_attribute_chain(attr_node: ast.Attribute) -> str:\n            parts = []\n            current = attr_node\n            while isinstance(current, ast.Attribute):\n                parts.append(current.attr)\n                current = current.value  # type: ignore\n            if isinstance(current, ast.Name):\n                # skip the root variable name (e.g., 'x')\n                pass\n            else:\n                raise NotImplementedError(f\"Unsupported attribute chain root: {type(current)}\")\n            # reverse to get the correct order\n            prop_path = \"/\".join(reversed(parts))\n            # Check if the top-level property is in the data model\n            top_level = parts[-1] if parts else None\n            if top_level and top_level not in self.definition.storage_names:\n                raise VectorStoreOperationException(\n                    f\"Field '{top_level}' not in data model (storage property names are used).\"\n                )\n            return prop_path\n\n        match node:\n            case ast.Compare():\n                if len(node.ops) > 1:\n                    values: list[ast.expr] = []\n                    for idx in range(len(node.ops)):\n                        if idx == 0:\n                            values.append(\n                                ast.Compare(","sourceCodeStart":629,"sourceCodeEnd":665,"githubUrl":"https://github.com/microsoft/semantic-kernel/blob/c028a0c7dc4f0814cdcbaba9d998f187a41197bf/python/semantic_kernel/connectors/azure_ai_search.py#L629-L665","documentation":"Raised inside _lambda_parser._parse_attribute_chain when the root of an attribute chain is not an ast.Name node. The parser walks a chain like x.a.b by following .value until it reaches the root; it expects that root to be an ast.Name (the lambda parameter, e.g. 'x'). If the chain is rooted on a call, subscript, constant, or any other expression, it cannot produce a valid OData field path and raises NotImplementedError.","triggerScenarios":"Writing a filter lambda whose left-hand attribute access is rooted on something other than the lambda variable — e.g. lambda x: some_obj.field == 1, lambda x: get(x).field == 1, or lambda x: x.items[0].field == 1 (subscript root). The parser only supports chains beginning at the lambda parameter name.","commonSituations":"Referencing a module-level or outer-scope object inside a filter lambda instead of the record parameter; chaining off a method call; using subscript indexing at the base of an attribute chain.","solutions":["Make sure every attribute chain in the filter lambda starts at the lambda parameter (e.g. lambda x: x.field == value).","Avoid calling functions or subscripting at the root of a field reference in the filter.","If you need a constant from outer scope, bind it as the comparator (right-hand side), not as part of the field path."],"exampleFix":"// before\noptions.filter = lambda x: helper.x.field == 1   # root is 'helper', not a Name\n\n// after\noptions.filter = lambda x: x.field == 1","handlingStrategy":"validation","validationCode":"import ast\n\ndef filter_lambdas_root_in_name(func):\n    tree = ast.parse(ast.unparse(ast.fix_missing_locations(\n        ast.parse(\"lambda x: None\", mode=\"eval\").body.replace(body=...)\n    ))) if False else None\n    # simpler: static lint that attribute chains in the lambda start at the lambda param\n    src = ast.unparse(ast.parse(ast.getsource(func.__code__), mode=\"exec\"))\n    return src  # review manually; chains must start at the param name\n\n# Enforce convention: all filter lambdas start chains at the lambda parameter","typeGuard":null,"tryCatchPattern":"from semantic_kernel.exceptions import VectorStoreException\ntry:\n    res = await collection.search(values=q, options=opts_with_filter)\nexcept NotImplementedError as e:\n    if \"Unsupported attribute chain root\" in str(e):\n        opts.filter = lambda x: x.field == value  # rewrite rooted at x\n        res = await collection.search(values=q, options=opts)\n    raise","preventionTips":["Root every attribute chain in a filter lambda at the lambda parameter (x.field, not obj.field).","Do not call functions or subscript at the base of a field reference in filters.","Keep filters simple: x.field <op> constant."],"tags":["filter","odata","lambda-parser","azure-ai-search"],"backgroundTag":null,"analyzedSha":"c028a0c7dc4f0814cdcbaba9d998f187a41197bf","analyzedAt":"2026-08-13T13:48:05.040Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}