Spreading activation retrieval for document collections
Project description
Pensive
Spreading activation retrieval for document collections. Sub-millisecond queries at 50M+ documents.
Pensive builds a sparse entity graph from your documents using regex-based extraction, then retrieves answers via spreading activation -- a biologically-inspired algorithm where query terms "light up" connected entities and the activation spreads to relevant answers.
Install
pip install pypensive # Core SA engine (numpy + scipy only)
pip install pypensive[full] # + L2 semantic search, BM25, hybrid retrieval
Quickstart
from pensive import SpreadingActivation
sa = SpreadingActivation()
sa.build([
{'id': '1', 'content': 'The P99 latency was 42ms on 2025-10-08', 'value': '42ms on 2025-10-08'},
{'id': '2', 'content': 'GPU temp hit 82C during the training run', 'value': '82C during training'},
{'id': '3', 'content': 'Meeting with Sarah Chen about Project Atlas budget', 'value': 'Atlas budget meeting'},
])
results = sa.query("What was the P99 latency?")
# [('42ms on 2025-10-08', 1.623), ...]
Parallel Build (large corpora)
# 14x faster at 1M docs using multiprocessing
sa = SpreadingActivation()
sa.build_parallel(documents, workers=8)
Ingestion from Data Exports
from pensive.ingestion import IngestPipeline
from pensive.ingestion.parsers.chatgpt import ChatGPTParser
from pensive.ingestion.parsers.facebook import FacebookParser
pipe = IngestPipeline()
pipe.ingest_all([
ChatGPTParser("/path/to/chatgpt-export/"),
FacebookParser("/path/to/facebook-export/"),
])
# Query
results = pipe.sa.query("What did we talk about last week?")
# Save/load
pipe.save_graph("my_graph.pkl")
pipe = IngestPipeline.load_graph("my_graph.pkl")
CLI
pensive build --chatgpt ~/chatgpt-export/ --facebook ~/fb-export/ -o graph.pkl
pensive query --graph graph.pkl "What was the deployment date?"
pensive stats --graph graph.pkl
Document Format
Each document is a dict with:
id(str): Unique identifiercontent(str): Text to extract entities fromvalue(str): The answer/snippet to retrievequery(str, optional): Additional text for entity extraction
Configuration
from pensive import SpreadingActivation, SpreadingConfig
sa = SpreadingActivation(config=SpreadingConfig(
max_hops=2, # Spreading depth (default: 4)
max_active=200, # Max active nodes per hop (default: 50)
decay=0.6, # Activation decay per hop (default: 0.6)
threshold=0.15, # Min activation to keep spreading (default: 0.15)
))
Contextual Disambiguation
Provide conversation context to disambiguate queries:
results = sa.query(
"What was the temperature?",
context=["GPU", "training run"] # Disambiguates toward GPU temp, not weather
)
Hybrid Retrieval (pypensive[full])
Default flow is two-stage retrieval:
- L1 SA generates fast candidate IDs.
- L2 FAISS reranks those L1 hits semantically.
If L1 returns nothing, hybrid can fall back to global L2 search.
from pensive import SpreadingActivation
from pensive.l2 import L2Handler
from pensive.parallel_hybrid import ParallelHybrid
sa = SpreadingActivation()
sa.build(documents)
l2 = L2Handler() # defaults to all-MiniLM-L6-v2
l2.add_documents(documents)
hybrid = ParallelHybrid(
spreading_activation=sa,
l2_handler=l2,
l2_on_sa_hits=True, # default
l2_fallback_global=True, # default
)
results = hybrid.query("What was the P99 latency on 2025-07-16?")
for r in results:
print(f"{r.doc_id}: {r.summary} (score={r.score:.1f}, source={r.source})")
BM25 Sparse Search
For keyword/identifier matching without embeddings:
from pensive.hybrid_search import BM25Index
idx = BM25Index()
idx.add_documents(documents)
results = idx.search("error 0x4F2A") # Exact identifier matching
What [full] adds
| Component | Purpose | Dependency |
|---|---|---|
| L2Handler | Semantic vector search | sentence-transformers, faiss-cpu |
| BM25Index | Sparse keyword matching | rank-bm25 |
| ParallelHybrid | SA + L2 agreement boosting | (uses both above) |
| Cross-encoder reranking | Optional reranker | sentence-transformers |
Scale Characteristics
| Scale | Query Latency | Peak RSS | Build Time (parallel) |
|---|---|---|---|
| 1M docs | ~1ms | 13 GB | ~12s |
| 5M docs | ~0.4ms | 16 GB | ~5 min |
| 10M docs | ~0.4ms | 30 GB | ~10 min |
| 50M docs | ~0.45ms | 139 GB | ~28 min |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pypensive-0.2.0.tar.gz.
File metadata
- Download URL: pypensive-0.2.0.tar.gz
- Upload date:
- Size: 56.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf393a3a622d1c4104dfa70694d4c7cedfd405f5de7ff2ca2c0948768a088935
|
|
| MD5 |
c6abb43a011364b63310bb56a1aa35f4
|
|
| BLAKE2b-256 |
6e01b23f1a2210db68b0e95e95649d931c9aa94339d731a98cfa443b989907b7
|
File details
Details for the file pypensive-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pypensive-0.2.0-py3-none-any.whl
- Upload date:
- Size: 57.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63983837d1bde595f5b1773d1edf2b02fc4cd0a9bdf52ad8914c0ad5c892ed91
|
|
| MD5 |
f702d14d5a3c040a32a271a44001a441
|
|
| BLAKE2b-256 |
6679b39d4327e3744d6539333d2508b73f947e4ce32124a9b46619a6bdbb6097
|