Skip to main content

oxibrain_core/
retrieval.rs

1//! Retrieval types: Query, TraversalSpec, and legacy store-side handles.
2//!
3//! The M8 `Retrieval` type and its `rank()` live in [`crate::rank`]. This
4//! module keeps the legacy `Query` / `QueryMode` / `SearchHit` / `SearchTarget`
5//! types that the pre-M8 `hybrid_query` path in `oxibrain-store::query`
6//! consumes. The presets in `crate::rank::Retrieval::hybrid()` etc. translate
7//! the same `QueryMode` strings into a `Retrieval` so the MCP `mode`
8//! parameter keeps working without a server-side rename (F29).
9
10use crate::knowledge::{EntityId, StatementId};
11use oxibrain_ports::Timestamp;
12use serde::{Deserialize, Serialize};
13
14// Re-export the M8 rank types so existing import paths
15// (`oxibrain_core::retrieval::RankingResult` etc.) continue to resolve.
16pub use crate::rank::{DropReason, DroppedItem, RankedItem, RankingResult};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Query {
20    pub text: String,
21    pub mode: QueryMode,
22    pub space: String,
23    #[serde(default)]
24    pub as_of: Option<Timestamp>,
25    #[serde(default = "default_limit")]
26    pub limit: usize,
27    #[serde(default)]
28    pub min_confidence: f32,
29}
30
31fn default_limit() -> usize {
32    20
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum QueryMode {
38    Hybrid,
39    Lexical,
40    LexicalVector,
41    /// Dense embedding KNN via sqlite-vec. Requires a configured embedder;
42    /// without one, querying in this mode returns an explicit error (§7.6).
43    Dense,
44    Graph,
45    Community,
46}
47
48impl QueryMode {
49    /// Translate the M7 string enum to an M8 preset name. The preset lives in
50    /// `crate::rank::Retrieval::hybrid/lexical/semantic/graph/community`.
51    pub fn to_preset(self) -> &'static str {
52        match self {
53            QueryMode::Hybrid => "hybrid",
54            QueryMode::Lexical => "lexical",
55            QueryMode::LexicalVector => "lexical", // TF-IDF KNN collapses into lexical
56            QueryMode::Dense => "semantic",
57            QueryMode::Graph => "graph",
58            QueryMode::Community => "community",
59        }
60    }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SearchHit {
65    pub target: SearchTarget,
66    pub score: f64,
67    pub mode: QueryMode,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(tag = "kind", rename_all = "snake_case")]
72pub enum SearchTarget {
73    Episode { id: String },
74    Statement { id: StatementId },
75    Entity { id: EntityId },
76}
77
78// --- Traversal ---
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct TraversalSpec {
82    pub start: Vec<EntityId>,
83    pub max_depth: u8,
84    pub max_nodes: u32,
85    pub predicates: PredicateFilter,
86    pub direction: Direction,
87    #[serde(default)]
88    pub valid_at: Option<Timestamp>,
89    pub min_confidence: f32,
90    pub strategy: Strategy,
91}
92
93impl Default for TraversalSpec {
94    fn default() -> Self {
95        Self {
96            start: Vec::new(),
97            max_depth: 3,
98            max_nodes: 256,
99            predicates: PredicateFilter::AllowAll,
100            direction: Direction::Both,
101            valid_at: None,
102            min_confidence: 0.0,
103            strategy: Strategy::Bfs,
104        }
105    }
106}
107// Direction and PredicateFilter live in oxibrain-index (spec.rs) per §18
108// rule 1 (core depends on index, not the reverse). Re-exported here so
109// existing `oxibrain_core::retrieval::Direction` paths continue to work.
110pub use oxibrain_index::{Direction, PredicateFilter};
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(tag = "kind", rename_all = "snake_case")]
114pub enum Strategy {
115    Bfs,
116    ShortestPath { to: EntityId },
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct TraversalResult {
121    pub nodes: Vec<TraversalNode>,
122    pub edges: Vec<TraversalEdge>,
123    pub truncated: bool,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct TraversalNode {
128    pub entity: EntityId,
129    pub depth: u8,
130    pub salience: f64,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct TraversalEdge {
135    pub from: EntityId,
136    pub to: EntityId,
137    pub predicate: String,
138    pub statement_id: StatementId,
139    pub depth: u8,
140}