Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,35 +131,41 @@ Replace `REGION` and `ACCOUNT_ID`. `dynamodb:Scan` is only needed for the GraphR

```python
from dynavec import Dynavec, DynavecConfig, Document
from dynavec.embeddings import OpenAIEmbedder # or Gemini / Bedrock / SentenceTransformer
from dynavec.embeddings import OpenAIEmbedder # or Gemini / Bedrock / SentenceTransformer

cfg = DynavecConfig(
vector_bucket="my-vectors", # S3 vector bucket
index="docs", # vector index
table="dynavec_docs", # DynamoDB table
vector_bucket="my-vectors", # S3 vector bucket
index="docs", # vector index
table="dynavec_docs", # DynamoDB table
dimension=1536,
distance_metric="cosine",
region="us-east-1",
auto_provision=True, # create bucket + index + table if missing
auto_provision=True, # create bucket + index + table if missing
)

db = Dynavec(cfg, embedder=OpenAIEmbedder(model="text-embedding-3-small"))

db.upsert(
[
Document(id="a", text="The mitochondria is the powerhouse of the cell.",
metadata={"topic": "biology", "year": 2021}),
Document(id="b", text="Rockets reach orbit at roughly 28,000 km/h.",
metadata={"topic": "space", "year": 2023}),
Document(
id="a",
text="The mitochondria is the powerhouse of the cell.",
metadata={"topic": "biology", "year": 2021},
),
Document(
id="b",
text="Rockets reach orbit at roughly 28,000 km/h.",
metadata={"topic": "space", "year": 2023},
),
],
auto_metadata=True, # also attach hash/timestamp/word counts
auto_metadata=True, # also attach hash/timestamp/word counts
)

hits = db.search(
"how do cells make energy?",
top_k=3,
filter={"topic": "biology"}, # S3 Vectors metadata pre-filter
rerank="mmr", # diversity-aware reranking
filter={"topic": "biology"}, # S3 Vectors metadata pre-filter
rerank="mmr", # diversity-aware reranking
)
for h in hits:
print(h.score, h.id, h.text)
Expand Down Expand Up @@ -190,10 +196,12 @@ Expire ephemeral documents, session memories, or cache entries automatically via

```python
# Pass ttl_seconds on Document or upsert()
db.upsert([
Document(id="session-1", text="Temporary session context", ttl_seconds=3600),
Document(id="cached-doc", text="Ephemeral cache data", ttl_seconds=86400),
])
db.upsert(
[
Document(id="session-1", text="Temporary session context", ttl_seconds=3600),
Document(id="cached-doc", text="Ephemeral cache data", ttl_seconds=86400),
]
)

# Or set a default TTL across an entire batch
ns = db.namespace("sessions")
Expand Down Expand Up @@ -259,11 +267,13 @@ from dynavec.integrations.tools import make_retriever_fn

retrieve = make_retriever_fn(db, namespace="kb", top_k=2)


@tool
def search_knowledge_base(query: str) -> str:
"""Search the dynavec knowledge base for relevant passages."""
return retrieve(query)


agent = Agent(tools=[search_knowledge_base])
agent("Where is dynavec vector data stored?")
```
Expand Down Expand Up @@ -426,7 +436,7 @@ from dynavec.dashboard import serve
rec = TelemetryRecorder()
db = Dynavec(cfg, embedder=emb, cache=SemanticCache(), telemetry=rec)
# ... your app runs searches; the recorder fills automatically ...
serve(rec, port=8779) # JSON API at http://127.0.0.1:8779
serve(rec, port=8779) # JSON API at http://127.0.0.1:8779
```

**2. Run the dashboard** (points at that API; falls back to sample data if unset):
Expand Down
37 changes: 8 additions & 29 deletions benchmarks/benchmark_opq.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

"""Benchmark PQ vs OPQ + PQ."""

from __future__ import annotations
Expand Down Expand Up @@ -63,9 +62,7 @@ def recall_at_k(
truth_set = set(truth[:k])
prediction_set = set(prediction[:k])

recalls.append(
len(truth_set & prediction_set) / k
)
recalls.append(len(truth_set & prediction_set) / k)

return float(np.mean(recalls))

Expand Down Expand Up @@ -115,9 +112,7 @@ def benchmark_quantizer(
k=10,
)

reconstruction_error = quantizer.reconstruction_error(
database
)
reconstruction_error = quantizer.reconstruction_error(database)

print(f"Fit time : {fit_time:.4f}s")
print(f"Encode time : {encode_time:.4f}s")
Expand Down Expand Up @@ -147,13 +142,9 @@ def main() -> None:
m = 8
nbits = 8

database = rng.normal(
size=(num_database, dimension)
).astype(np.float32)
database = rng.normal(size=(num_database, dimension)).astype(np.float32)

queries = rng.normal(
size=(num_queries, dimension)
).astype(np.float32)
queries = rng.normal(size=(num_queries, dimension)).astype(np.float32)

print("=== PQ vs OPQ + PQ Benchmark ===")
print(f"Database : {num_database} vectors")
Expand Down Expand Up @@ -223,25 +214,13 @@ def main() -> None:

print("\n=== Comparison ===")

recall_improvement = (
opq_results["recall_10"]
- pq_results["recall_10"]
)
recall_improvement = opq_results["recall_10"] - pq_results["recall_10"]

error_improvement = (
pq_results["reconstruction_error"]
- opq_results["reconstruction_error"]
)
error_improvement = pq_results["reconstruction_error"] - opq_results["reconstruction_error"]

print(
f"Recall@10 improvement : "
f"{recall_improvement:+.4f}"
)
print(f"Recall@10 improvement : {recall_improvement:+.4f}")

print(
f"Reconstruction improvement: "
f"{error_improvement:+.6f}"
)
print(f"Reconstruction improvement: {error_improvement:+.6f}")


if __name__ == "__main__":
Expand Down
45 changes: 10 additions & 35 deletions benchmarks/benchmark_quantization.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ def generate_dataset(n_vectors=10_000, dim=32, seed=0):
centers = rng.normal(size=(8, dim)).astype(np.float32)
assign = rng.integers(0, 8, size=n_vectors)

x = centers[assign] + 0.05 * rng.normal(
size=(n_vectors, dim)
).astype(np.float32)
x = centers[assign] + 0.05 * rng.normal(size=(n_vectors, dim)).astype(np.float32)

return x.astype(np.float32)

Expand Down Expand Up @@ -94,15 +92,11 @@ def benchmark():
scalar_codes,
)

scalar_reconstruction_error = scalar.reconstruction_error(
vectors
)
scalar_reconstruction_error = scalar.reconstruction_error(vectors)

scalar_bytes_per_vector = scalar.code_size_bytes
scalar_total_bytes = scalar_codes.nbytes
scalar_compression = (
raw_bytes_per_vector / scalar_bytes_per_vector
)
scalar_compression = raw_bytes_per_vector / scalar_bytes_per_vector

# ---------------------------------------------------------
# Product Quantizer
Expand Down Expand Up @@ -130,15 +124,11 @@ def benchmark():
pq_codes,
)

pq_reconstruction_error = pq.reconstruction_error(
vectors
)
pq_reconstruction_error = pq.reconstruction_error(vectors)

pq_bytes_per_vector = pq.code_size_bytes
pq_total_bytes = pq_codes.nbytes
pq_compression = (
raw_bytes_per_vector / pq_bytes_per_vector
)
pq_compression = raw_bytes_per_vector / pq_bytes_per_vector

# ---------------------------------------------------------
# Results
Expand All @@ -148,10 +138,7 @@ def benchmark():
print(f" Fit time: {scalar_fit_time:.4f} sec")
print(f" Encode time: {scalar_encode_time:.4f} sec")
print(f" Decode time: {scalar_decode_time:.4f} sec")
print(
f" Reconstruction error: "
f"{scalar_reconstruction_error:.8f}"
)
print(f" Reconstruction error: {scalar_reconstruction_error:.8f}")
print(f" Bytes/vector: {scalar_bytes_per_vector}")
print(f" Total encoded size: {scalar_total_bytes / 1024:.2f} KB")
print(f" Compression: {scalar_compression:.2f}x")
Expand All @@ -161,32 +148,20 @@ def benchmark():
print(f" Fit time: {pq_fit_time:.4f} sec")
print(f" Encode time: {pq_encode_time:.4f} sec")
print(f" Decode time: {pq_decode_time:.4f} sec")
print(
f" Reconstruction error: "
f"{pq_reconstruction_error:.8f}"
)
print(f" Reconstruction error: {pq_reconstruction_error:.8f}")
print(f" Bytes/vector: {pq_bytes_per_vector}")
print(f" Total encoded size: {pq_total_bytes / 1024:.2f} KB")
print(f" Compression: {pq_compression:.2f}x")

print()
print("Comparison")
print(
f" Scalar/PQ compression: "
f"{scalar_compression / pq_compression:.2f}x"
)
print(f" Scalar/PQ compression: {scalar_compression / pq_compression:.2f}x")

if scalar_encode_time > 0:
print(
f" PQ encode / Scalar encode: "
f"{pq_encode_time / scalar_encode_time:.2f}x"
)
print(f" PQ encode / Scalar encode: {pq_encode_time / scalar_encode_time:.2f}x")

if scalar_decode_time > 0:
print(
f" PQ decode / Scalar decode: "
f"{pq_decode_time / scalar_decode_time:.2f}x"
)
print(f" PQ decode / Scalar decode: {pq_decode_time / scalar_decode_time:.2f}x")

print()
print("=" * 80)
Expand Down
21 changes: 12 additions & 9 deletions benchmarks/cost_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@

@dataclass
class Workload:
vectors: int # number of stored vectors
dim: int # embedding dimension
vectors: int # number of stored vectors
dim: int # embedding dimension
queries_per_month: int
writes_per_month: int = 0
metadata_bytes: int = 500 # avg full metadata + text per doc (DynamoDB)
filterable_bytes: int = 120 # avg filterable metadata per vector (S3 Vectors)
metadata_bytes: int = 500 # avg full metadata + text per doc (DynamoDB)
filterable_bytes: int = 120 # avg filterable metadata per vector (S3 Vectors)

@property
def vector_bytes(self) -> int:
Expand Down Expand Up @@ -147,12 +147,15 @@ def main() -> None:
p.add_argument("--wpm", type=int, default=100_000, help="writes per month")
args = p.parse_args()

w = Workload(vectors=args.vectors, dim=args.dim,
queries_per_month=args.qpm, writes_per_month=args.wpm)
w = Workload(
vectors=args.vectors, dim=args.dim, queries_per_month=args.qpm, writes_per_month=args.wpm
)

print(f"\nWorkload: {w.vectors:,} vectors × {w.dim}d "
f"({w.raw_vector_gb:.1f} GB raw), {w.queries_per_month:,} queries/mo, "
f"{w.writes_per_month:,} writes/mo\n")
print(
f"\nWorkload: {w.vectors:,} vectors × {w.dim}d "
f"({w.raw_vector_gb:.1f} GB raw), {w.queries_per_month:,} queries/mo, "
f"{w.writes_per_month:,} writes/mo\n"
)
print("dynavec breakdown:")
for k, v in dynavec_cost(w).items():
print(f" {k:<22} ${v:>10,.2f}")
Expand Down
6 changes: 3 additions & 3 deletions benchmarks/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

@dataclass
class Dataset:
vectors: np.ndarray # (N, dim) float32
queries: np.ndarray # (Q, dim) float32
vectors: np.ndarray # (N, dim) float32
queries: np.ndarray # (Q, dim) float32
ids: list[str]
ground_truth: np.ndarray # (Q, k) int indices into vectors (exact NN)

Expand Down Expand Up @@ -55,7 +55,7 @@ def _l2_normalize(x: np.ndarray) -> np.ndarray:

def _exact_topk(vectors: np.ndarray, queries: np.ndarray, k: int) -> np.ndarray:
# cosine similarity == dot product on L2-normalized vectors
sims = queries @ vectors.T # (Q, N)
sims = queries @ vectors.T # (Q, N)
return np.argpartition(-sims, kth=k - 1, axis=1)[:, :k]


Expand Down
Loading
Loading