Skip to main content

POC: query-conditional poison vectors for vector search relevance feedback

Project description

Poison Vectors

A vector-store MCP server with built-in negative-feedback learning. Index documents, search semantically — and when a result is a false match, flag it so that searches similar to the one that found it also exclude it, without harming queries where that document is genuinely relevant. Backed by your choice of store: in-memory, flat-file, SQLite (sqlite-vec), or Qdrant.

"Poison" here is benign — a stored negative-feedback signal, not the adversarial data-poisoning attacks the term usually means in ML.

This repository is the full arc: a validated research result, a packaged Python library, a numerically-certified C# port, and a published MCP server an agent can call over dnx.

The result, in one table

The headline metric is collateral damage — does suppressing a false match here harm the queries where that document is relevant? Measured on real homonym data (CoarseWSD-20, held-out nDCG@10 delta; positive = net benefit, worst = the collateral floor):

Mechanism λ=1 mean / worst λ=5 mean / worst
naive (global suppression) −0.115 / −0.83 −0.648 / −1.00
query-conditional +0.001 / −0.31 −0.027 / −1.00
contrastive (separating-direction) +0.003 / −0.23 +0.004 / −0.26

Verdict: GO. Query-conditional poisoning controls the collateral damage that naive global suppression inflicts; the contrastive (separating-direction) mechanism is the standout — the only one that stays net-positive with a bounded worst-case (never the −1.0 wipeout), robust to its strength parameter. Validated on a falsifiable synthetic corpus and confirmed on real data (SciFact + homonyms).

Full numbers and methodology: Research & Results.

Seeing it work (live, through the MCP server)

A word-sense example — "python" the language vs. the snake. Six docs indexed; the query "best way to learn python programming for data analysis" initially ranks two snake docs above a genuinely-relevant programming doc. After one record_false_match on the top snake doc (anchored to a good doc):

Doc Before After feedback A dissimilar query
("care for a pet snake")
prog-1 (language) 0.67 (#1) 0.67 (#1) ✓ 0.11
prog-2 (language) 0.27 (#4) 0.16 (#2) 0.16
snake-2 (flagged) 0.35 (#2) −2.49 (last) 0.51 (#1)
snake-1 (not flagged) 0.32 (#3) −1.31(generalized) 0.50 (#2)
monty-1 (not flagged) 0.19 −0.59(generalized) 0.15

Feedback on one doc generalizes to the whole wrong sense and to similar future queries, while the same docs stay top-ranked for a dissimilar query where they are correct — query-conditional suppression, zero collateral.

How it works (the short version)

Each mechanism subtracts a non-negative penalty from the base cosine score. The winning mechanism penalizes a candidate's lean along the separating direction between the flagged false match and an anchor (the query, or a known good doc), gated by how similar the new query is to the one that flagged it:

score(d) = sim(q, d) − λ · maxₚ [ gate(sim(q, q_trigₚ)) · max(0, sim(d, sₚ)) ]
           where sₚ = normalize(bad_p − anchor)

So suppression radiates outward from the original query and fades for unrelated queries — and only along the axis that made the match wrong, leaving relevant neighbours intact. Two empirical corrections are load-bearing in every mechanism: penalties are clipped at 0 (never boost a doc) and poisons aggregate by max (not sum). Details: Design & Implementation.

What's in this repo

Component What it is Where
poison-vectors (Python, 0.2.0) The research testbed and the packaged library: PoisonStore overlay, mechanisms, JSON persistence. NumPy-only core. poison_vectors/, pyproject.toml
PoisonVectors (C#, net10.0) A numerically-certified port of the scoring engine — VecMath, the three mechanisms, PoisonStore, the JSON persistence envelope. csharp/PoisonVectors/
PoisonVectors MCP server The agent-facing vector store: an ONNX MiniLM embedder + a pluggable datastore (in-memory, flat-file, SqliteVec, or Qdrant) exposed over MCP (stdio), published to NuGet and runnable via dnx. Semantic search with a negative-feedback (poison) overlay. csharp/PoisonVectors.Cli/

The Python and C# implementations are kept equivalent by golden cross-checks: the C# rerank reproduces Python's scores to ~1e-9, the persistence envelope round-trips both directions (Python↔C#), and the C# ONNX embedder matches Python sentence-transformers/all-MiniLM-L6-v2 to ~6 decimals.

Quickstart

Python — research & library

pip install -e ".[dev]"
python -m pytest -q                                   # 77 tests
python -m poison_vectors.experiments.milestone_a      # synthetic go/no-go
python examples/ann_overlay.py                        # worked ANN-overlay example

pip install -e ".[real]"                              # for real-data runs
python -m poison_vectors.experiments.coarsewsd_run    # real homonyms (CoarseWSD-20)
from poison_vectors import PoisonStore, Mechanism

store = PoisonStore(mechanism=Mechanism.CONTRASTIVE, lam=5.0)
store.add_feedback(query_vec, bad_doc_vec, good_doc_vec)      # vectors in
reranked = store.rerank(query_vec, candidates=[(doc_id, vec), ...], k=10)

C# — library & tests

cd csharp
dotnet test PoisonVectors.sln          # 104 tests (net10.0)

MCP server — run via dnx (needs the .NET 10 SDK)

The server is published to NuGet as an mcpserver tool:

dnx PoisonVectors --yes -- serve        # MCP server over stdio (what a client launches)
dnx PoisonVectors -- embed "some text"  # quick embed check
dnx PoisonVectors -- demo               # before/after rerank demo

First run downloads the all-MiniLM-L6-v2 ONNX model (~90 MB) to a local cache (%LOCALAPPDATA%/poison-vectors/models/all-MiniLM-L6-v2, overridable via POISON_VECTORS_MODEL_DIR).

Register it in a client with the built-in setup command (interactive by default; flags for automation):

dnx PoisonVectors -- setup --client claude --scope project-local --yes

This writes/merges a project-scoped .mcp.json so Claude Code can launch the server. MCP tools exposed: index_document, search, remove_document, record_false_match (optional good-doc anchor; returns a record id), list_poisons, get_poison, search_poisons, remove_poison, remove_poisons_for_doc, clear_poisons, save_store/load_store (full corpus+poison snapshot export/import).

Full guides: Usage (Python + C#/MCP).

Storage providers & configuration

The MCP server persists both the corpus and the poison-record set to a pluggable datastore. Select it with an environment variable, overridable by a serve flag (precedence: flag > env var > default).

Setting Env var serve flag Default
Provider POISON_VECTORS_STORE --store flatfile
Flat-file directory POISON_VECTORS_STORE_DIR --store-dir ~/.poison-vectors
SqliteVec file POISON_VECTORS_STORE_FILE --store-file ~/.poison-vectors/poison-vectors.db
Qdrant host POISON_VECTORS_QDRANT_HOST --qdrant-host localhost
Qdrant port (gRPC) POISON_VECTORS_QDRANT_PORT --qdrant-port 6334
Qdrant API key POISON_VECTORS_QDRANT_API_KEY --qdrant-api-key (none)

Valid providers: memory, flatfile, sqlitevec, qdrant. The default, flatfile, persists both the corpus and the poison records under ~/.poison-vectorsstate is durable and shared across launches. sqlitevec is now available — ANN-backed via the sqlite-vec extension; select it with --store sqlitevec (db file at POISON_VECTORS_STORE_FILE, default ~/.poison-vectors/poison-vectors.db). Selecting sqlitevec downloads the sqlite-vec v0.1.9 native extension on first use to ~/.poison-vectors/native/ (cache dir overridable via POISON_VECTORS_VEC0_DIR; point at a prebuilt extension with POISON_VECTORS_VEC0_PATH). qdrant is available — ANN-backed via the official Qdrant.Client (gRPC); select it with --store qdrant. The qdrant provider expects a reachable Qdrant server (e.g. docker run -p 6334:6334 qdrant/qdrant).

Documentation

  • Research & Results — concept, research question, prior art, methodology, all milestone results, references.
  • Design & Implementation — architecture, mechanism math, metrics, the falsifiable synthetic corpus, module map, how to extend.
  • Usage — install, run experiments, use as a library, the C# port, and the MCP server / dnx / setup.
  • Roadmap — the productization plan and its delivery status.
  • Process artifacts (specs, plans, milestone findings): docs/superpowers/.

Status

All three roadmap phases are shipped: the Python module, the C# port (certified equivalent), and the MCP server (published, dnx-distributed, exercised live in Claude Code). The server is now being expanded into a pluggable vector store: the IVectorStore abstraction, addressable poison records (per-id and per-doc removal, get/search), remove_document, and env+CLI store configuration are in place; the flat-file provider is now durable (v2 snapshot: corpus + poison records persist under ~/.poison-vectors); the SqliteVec provider is now available (ANN via sqlite-vec; --store sqlitevec); the Qdrant provider is now available (ANN via Qdrant.Client; --store qdrant). All four providers ship. Licensed under PolyForm Noncommercial 1.0.0 (see LICENSE).

Data & licensing

The synthetic corpus is generated in-process. Real-data loaders download on demand: SciFact (BEIR) and CoarseWSD-20 (homonyms, CC BY-NC 4.0, NonCommercial — research only; downloaded to a gitignored cache, never committed). The repository is licensed under the PolyForm Noncommercial 1.0.0 license — noncommercial use and modification are permitted; no warranty is provided. Future versions may be released under different terms.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

poison_vectors-0.2.0.tar.gz (42.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

poison_vectors-0.2.0-py3-none-any.whl (36.4 kB view details)

Uploaded Python 3

File details

Details for the file poison_vectors-0.2.0.tar.gz.

File metadata

  • Download URL: poison_vectors-0.2.0.tar.gz
  • Upload date:
  • Size: 42.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for poison_vectors-0.2.0.tar.gz
Algorithm Hash digest
SHA256 be423b3013d3c09f134361a1772088e7a0d4344ffaf0b5ae1767ec9d7da0c3bf
MD5 73e756ec1031b55a718be11321d02f12
BLAKE2b-256 011e6faf769ed28edb0eb3a7ae6ef5c0cb678fbcabaacc925fd39e8ab93cf1c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for poison_vectors-0.2.0.tar.gz:

Publisher: publish-pypi.yml on jamesburton/poison-vectors

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file poison_vectors-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: poison_vectors-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 36.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for poison_vectors-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e5ac11a49e2c43425df01d5f7b94415be2721ddb5f3d4310bc3c3764ffa6cb96
MD5 2b15571659d4cdc220406a1c4d711742
BLAKE2b-256 92699684a79a0d115bef2eea763719bd832dcec973e96e86ba384d31652feefd

See more details on using hashes here.

Provenance

The following attestation bundles were made for poison_vectors-0.2.0-py3-none-any.whl:

Publisher: publish-pypi.yml on jamesburton/poison-vectors

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page