opensolr-haystack 0.3.1__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opensolr-haystack
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Haystack integration for Opensolr — managed Apache Solr DocumentStore with server-side embeddings and hybrid BM25+kNN retrieval
5
5
  Author-email: Opensolr <support@opensolr.com>
6
6
  License: MIT
@@ -66,6 +66,41 @@ BATCH_EMBED_MAX = 50
66
66
  # so it is applied at full strength with no hedging.
67
67
  FRESH_BIAS_FUNCTION = "recip(max(0,ms(NOW,creation_date)),3.16e-11,1,1)"
68
68
 
69
+ #: Default Fresh Results Bias strength when a caller does not pass one.
70
+ FRESH_BIAS_WEIGHT_DEFAULT = 0.5
71
+
72
+
73
+ def fresh_bias_function(weight: Optional[float] = None) -> str:
74
+ """Build the recency function for a 0.0-1.0 ``weight``.
75
+
76
+ Mirrors ``Hybrid_search::fresh_bias_function()`` on opensolr.com, the Drupal module and
77
+ the WordPress plugin. ``recip(ms, c, 1, 1)`` halves at ``ms = 1/c``, so the weight is a
78
+ HALF-LIFE on a geometric scale between 365 days at 0.0 and 6 hours at 1.0:
79
+
80
+ ========== ===========================================================
81
+ weight meaning
82
+ ========== ===========================================================
83
+ 0.0 365-day half-life: technically on, barely visible
84
+ 0.3 41 days
85
+ 0.5 9.6 days (the default)
86
+ 0.7 2.2 days
87
+ 1.0 6 hours: date all but replaces relevance
88
+ ========== ===========================================================
89
+
90
+ The fixed constant this replaces behaved like 0.0, which is why Fresh looked broken on a
91
+ news index: a 10-day-old article kept 97% of its multiplier, nowhere near enough to
92
+ outrank a better-matching older one.
93
+ """
94
+ w = FRESH_BIAS_WEIGHT_DEFAULT if weight is None else float(weight)
95
+ w = max(0.0, min(1.0, w))
96
+ half_life_days = 365.0 * (0.25 / 365.0) ** w
97
+ # PHP's %g writes "1.212e-9" where Python's writes "1.212e-09". Numerically identical,
98
+ # textually not — and this string is compared byte for byte against the platform, the
99
+ # Drupal module and the WordPress plugin by the prompt/query parity harness. Strip the
100
+ # padding zero so all five implementations emit the same characters.
101
+ c = ("%.4g" % (1.0 / (half_life_days * 86400000.0))).replace("e-0", "e-").replace("e+0", "e+")
102
+ return "recip(max(0,ms(NOW,creation_date))," + c + ",1,1)"
103
+
69
104
  #: The four candidate-selection modes the {!hybrid} parser understands.
70
105
  #:
71
106
  #: Validated rather than trusted, because the failure is silent: `mode` is interpolated into
@@ -77,7 +112,7 @@ FRESH_BIAS_FUNCTION = "recip(max(0,ms(NOW,creation_date)),3.16e-11,1,1)"
77
112
  HYBRID_MODES = ("union", "keywords_required", "meaning_required", "intersection")
78
113
 
79
114
 
80
- def apply_fresh_bias(params: Dict[str, Any]) -> Dict[str, Any]:
115
+ def apply_fresh_bias(params: Dict[str, Any], weight: Optional[float] = None) -> Dict[str, Any]:
81
116
  """Wrap an already-built ``params["q"]`` so the recency curve multiplies the
82
117
  FINAL score. Mutates ``params`` in place and returns it.
83
118
 
@@ -99,7 +134,21 @@ def apply_fresh_bias(params: Dict[str, Any]) -> Dict[str, Any]:
99
134
  rather than being inlined, so a ``}`` in the user's text cannot close the
100
135
  ``{!boost}`` block and leave the remainder to be parsed as query syntax.
101
136
  """
102
- params["freshBias"] = FRESH_BIAS_FUNCTION
137
+ # A document with no creation_date evaluates recip() at its MAXIMUM, 1.0 — Solr's
138
+ # ms(NOW, <missing>) is 0 — so an undated document is scored as if published this
139
+ # instant and floats to the top of a "newest first" ranking. Require a date instead
140
+ # of silently promoting the ones that have none.
141
+ fq = params.get("fq")
142
+ date_fq = "+creation_date:[* TO *]"
143
+ if fq is None:
144
+ params["fq"] = date_fq
145
+ elif isinstance(fq, list):
146
+ if date_fq not in fq:
147
+ params["fq"] = fq + [date_fq]
148
+ elif fq != date_fq:
149
+ params["fq"] = [fq, date_fq]
150
+
151
+ params["freshBias"] = fresh_bias_function(weight)
103
152
  params["freshBiasInner"] = params["q"]
104
153
  params["q"] = "{!boost b=$freshBias v=$freshBiasInner}"
105
154
  return params
@@ -706,6 +755,7 @@ class OpensolrClient:
706
755
  fl: str = "*,score",
707
756
  fq: Optional[str] = None,
708
757
  fresh_bias: bool = False,
758
+ fresh_bias_weight: Optional[float] = None,
709
759
  ) -> Dict[str, Any]:
710
760
  """Hybrid (BM25 + kNN) search via the native ``{!hybrid}`` parser.
711
761
 
@@ -745,7 +795,7 @@ class OpensolrClient:
745
795
  if fq:
746
796
  params["fq"] = fq
747
797
  if fresh_bias:
748
- apply_fresh_bias(params)
798
+ apply_fresh_bias(params, fresh_bias_weight)
749
799
  return self.solr_select(index, params)
750
800
 
751
801
  #: RAG context defaults — how many hybrid hits feed the LLM, and how many
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opensolr-haystack
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Haystack integration for Opensolr — managed Apache Solr DocumentStore with server-side embeddings and hybrid BM25+kNN retrieval
5
5
  Author-email: Opensolr <support@opensolr.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "opensolr-haystack"
7
- version = "0.3.1"
7
+ version = "0.4.0"
8
8
  description = "Haystack integration for Opensolr — managed Apache Solr DocumentStore with server-side embeddings and hybrid BM25+kNN retrieval"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }