lunaris_retrieve/operators/
keyword.rs1use std::any::Any;
9
10use async_trait::async_trait;
11use lunaris_core::LunarisError;
12
13use super::{QueryContext, Retriever, clamp_k};
14use crate::types::{RawHit, SourceOp};
15
16#[derive(Clone, Debug)]
18#[must_use = "Keyword is a query node — pass it to RetrievalBuilder::with_root() or chain via .and/.or/.fuse_rrf, otherwise it never executes"]
19pub struct Keyword {
20 pub index: String,
21 pub k: usize,
22}
23
24impl Keyword {
25 pub fn bm25(index: impl Into<String>, k: usize) -> Self {
27 Self { index: index.into(), k: clamp_k(k) }
28 }
29
30 pub fn and<R: Retriever + 'static>(self, other: R) -> super::combinators::AndRetriever {
31 super::combinators::AndRetriever::new(Box::new(self), Box::new(other))
32 }
33 pub fn or<R: Retriever + 'static>(self, other: R) -> super::combinators::OrRetriever {
34 super::combinators::OrRetriever::new(Box::new(self), Box::new(other))
35 }
36 pub fn then<R: Retriever + 'static>(self, other: R) -> super::combinators::ThenRetriever {
37 super::combinators::ThenRetriever::new(Box::new(self), Box::new(other))
38 }
39 pub fn top(self, n: usize) -> super::modifiers::TopRetriever {
40 super::modifiers::TopRetriever::new(Box::new(self), n)
41 }
42
43 pub fn rerank(
45 self,
46 reranker: std::sync::Arc<dyn lunaris_rerank::Reranker>,
47 ) -> super::rerank::RerankRetriever {
48 super::rerank::RerankRetriever::new(Box::new(self), reranker)
49 }
50
51 pub fn degraded_fallback<R: Retriever + 'static>(
54 self,
55 fallback: R,
56 ) -> super::degraded::DegradedFallbackRetriever {
57 super::degraded::DegradedFallbackRetriever::new(Box::new(self), Box::new(fallback))
58 }
59}
60
61#[async_trait]
62impl Retriever for Keyword {
63 async fn retrieve(&self, ctx: &QueryContext) -> Result<Vec<RawHit>, LunarisError> {
64 let hits = crate::missing_index::no_rows_if_index_absent(
70 ctx.keyword
71 .keyword_search(
72 &ctx.scope,
73 &self.index,
74 &ctx.query.text,
75 self.k,
76 ctx.query.filter.as_ref(),
77 ctx.query.as_of,
78 )
79 .await,
80 )?;
81 Ok(hits
82 .into_iter()
83 .map(|h| RawHit {
84 id: h.id,
85 score: h.score,
86 rerank_applied: false,
87 degraded: false,
88 metadata: super::tag_leg_index(h.metadata, &self.index),
89 source_op: SourceOp::Keyword,
90 })
91 .collect())
92 }
93
94 fn as_any(&self) -> &dyn Any {
95 self
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn keyword_bm25_records_fields() {
105 let k = Keyword::bm25("chunks", 30);
106 assert_eq!(k.index, "chunks");
107 assert_eq!(k.k, 30);
108 }
109
110 #[test]
111 fn keyword_k_is_clamped() {
112 let k = Keyword::bm25("chunks", 1_000_000);
113 assert_eq!(k.k, super::super::MAX_K);
114 }
115}