popoto.fields.decaying_sorted_field¶
popoto.fields.decaying_sorted_field
¶
DecayingSortedField — time-weighted scoring via Lua decay computation.
This module provides a SortedField subclass where records lose relevance over time following power-law decay: base_score * elapsed_days^(-decay_rate).
The sorted set stores timestamps as scores. A Lua script computes decay-ranked results at query time, reading base scores from each member's model hash via cmsgpack.
Design
- Timestamps are always stored as scores (auto_now=True behavior)
- Decay computation happens server-side in Lua (no round trips)
- Base scores come from a companion field on the same model hash
- When base_score_field is None, all items have equal base score (1.0)
Example
class Memory(Model): key = UniqueKeyField() content = StringField() strength = FloatField(default=1.0) last_accessed = DecayingSortedField( decay_rate=0.5, base_score_field="strength", )
Query top-10 memories by decayed relevance¶
top = Memory.query.top_by_decay("last_accessed", n=10)
Refresh a memory's decay clock without full save¶
memory.touch("last_accessed")
DecayingSortedField
¶
Bases: SortedFieldMixin, Field
A SortedField subclass where records lose relevance over time.
Stores timestamps as sorted set scores. A Lua script computes decay-ranked results at query time using power-law decay: decayed_score = base_score * elapsed_days ^ (-decay_rate)
With decay_rate=0.5, a record scores 1.0 after 1 day, 0.5 after 4 days, and 0.1 after 100 days.
Ranking is deterministic: equal-scored members are ordered by member key (redis_key) ascending, byte-wise, broken inside the Lua script before top-N truncation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
decay_rate
|
Controls how fast scores drop. Higher = faster decay.
Defaults to |
required | |
base_score_field
|
Name of a companion field whose value multiplies the decay curve. When None, base score is 1.0. |
required | |
confidence_modulation_field
|
Controls confidence-modulated decay
(issue #491). |
required | |
partition_by
|
Partition the sorted set by key field values. Inherited from SortedFieldMixin. |
required |
Source code in src/popoto/fields/decaying_sorted_field.py
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | |
rank_decayed(zset_key, *, now, n=None, confidence=None, validity=None, decay_rate=None, base_score_field=None)
¶
Evaluate this field's decay script over one sorted set (#648).
This method owns the KEYS array so that callers do not. Before it
existed, every EVAL of a decay script was assembled by a caller --
models/query.py and recipes/context_assembler.py each held their
own copy of both layouts, four copies in total of a mapping whose
misapplication corrupts silently rather than erroring (see the do not
"unify" them note inside :data:DECAY_SCORE_LUA).
The two layouts are deliberately not unified into a single body with
a flag. :class:CyclicDecayField overrides this method with its own,
so neither implementation contains an index it must not use: this one
knows confidence is KEYS[2] and knows nothing about cycles; the
override knows cycles/pressure are KEYS[2]/KEYS[3] and knows
nothing about the validity gate. The rule the script comments ask
readers to respect is enforced by the class boundary instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
zset_key
|
str
|
The (already partition-resolved) sorted set to rank. |
required |
now
|
float
|
Current epoch seconds, passed as |
required |
n
|
Optional[int]
|
Max members to return. |
None
|
confidence
|
Optional[tuple[str, str, str]]
|
|
None
|
validity
|
Optional[tuple[str, str, str]]
|
|
None
|
decay_rate
|
Optional[float]
|
Override |
None
|
base_score_field
|
Optional[str]
|
Override |
None
|
Returns:
| Type | Description |
|---|---|
list[Any]
|
The script's raw flat reply, |
list[Any]
|
undecoded. Callers already differ in how they decode and |
list[Any]
|
normalizing it here would change what their parsing loops receive. |
Source code in src/popoto/fields/decaying_sorted_field.py
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | |
resolve_confidence_modulation_field(model_class, field, field_name)
¶
Resolve which ConfidenceField modulates field's decay.
Resolution order (first match wins):
Defaults.DECAY_CONFIDENCE_MODULATION_ENABLED is False-> off. This is the deploy-level kill switch: it disables modulation without any model-code edit, for adopters who cannot edit model definitions.confidence_modulation_field=False-> off (per-field opt-out).confidence_modulation_field="name"-> that field, orModelExceptionif it is missing or is not aConfidenceField.confidence_modulation_field=None(default) -> auto-detect overmodel_class._meta.fields. Exactly oneConfidenceFieldis used; zero means off; two or more means off plus a warning naming the candidates. Guessing between two confidence signals would silently pick a ranking policy the adopter never chose.
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
Any
|
|
Any
|
|
Source code in src/popoto/fields/decaying_sorted_field.py
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 | |
confidence_modulation_args(model_class, field, field_name, *, filters=None, model_instance=None)
¶
Build (confidence_hash_key, s, c0) for a decay EVAL.
c0 is the resolved field's own initial_confidence -- never a
hard-coded 0.5. It is both the absent-value default and the centering
constant, so an adopter running initial_confidence=0.3 still gets a
bit-exactly neutral score for a record with no evidence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_class
|
Any
|
The Model class being queried. |
required |
field
|
Any
|
The DecayingSortedField / CyclicDecayField instance. |
required |
field_name
|
str
|
Name of that field. |
required |
filters
|
Optional[dict[Any, Any]]
|
Query filter mapping, used to satisfy the ConfidenceField's
|
None
|
model_instance
|
Any
|
A saved instance to read partition values off of, for callers (the metacognitive proxy) that have records but no filters. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[str, str, str]
|
tuple[str, str, str]: |
Raises:
| Type | Description |
|---|---|
QueryException
|
The ConfidenceField is partitioned by fields the query
does not filter on, so no single |
Source code in src/popoto/fields/decaying_sorted_field.py
resolve_validity_field_name(model_class)
¶
Return the name of the model's ValidityField, or None.
First declared field wins, matching the auto-detection style used for the
ConfidenceField / BM25Field / TagField seams elsewhere. Returns None
for models with no validity axis, which is every model that has not opted
in — the overwhelmingly common case, and the one that must stay free.
Source code in src/popoto/fields/decaying_sorted_field.py
validity_gate_args(model_class, as_of=None)
¶
Build (invalid_at_key, valid_from_key, as_of) for a decay EVAL.
The single resolver behind all three production DECAY_SCORE_LUA call
sites, so there is exactly one place that knows the gate's KEYS/ARGV order
(KEYS[3] = invalid_at, KEYS[4] = valid_from, ARGV[7] =
as-of).
Defaults.VALIDITY_GATING_ENABLED is read here, at call time, never
captured at import time: the kill switch has to take effect at runtime for
adopters who cannot edit model code (issue #580, plan D6).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_class
|
Any
|
The Model class being queried. |
required |
as_of
|
Optional[float]
|
Epoch seconds to evaluate membership at. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
tuple[str, str, str]: |
str
|
the model declares no |