Skip to main content

sqlite_graphrag/constants/
search.rs

1//! Retrieval tuning and the agent-native surface vocabulary.
2//!
3//! Split out of the former single-file `constants.rs` in v1.2.5;
4//! every item is re-exported by the parent module, so `crate::constants::X`
5//! resolves exactly as before.
6
7/// Jaccard threshold above which two memories are considered fuzzy duplicates.
8pub const DEDUP_FUZZY_THRESHOLD: f64 = 0.8;
9
10/// Cosine distance threshold below which two memories are semantic duplicates.
11pub const DEDUP_SEMANTIC_THRESHOLD: f32 = 0.1;
12
13/// Maximum number of hops allowed in graph traversals.
14pub const MAX_GRAPH_HOPS: u32 = 2;
15
16/// Minimum relationship weight required for traversal inclusion.
17pub const MIN_RELATION_WEIGHT: f64 = 0.3;
18
19/// Default traversal depth for `related` when `--hops` is omitted.
20pub const DEFAULT_MAX_HOPS: u32 = 2;
21
22/// Default minimum weight filter applied during graph traversal.
23pub const DEFAULT_MIN_WEIGHT: f64 = 0.3;
24
25/// Default weight assigned to newly created relationships.
26pub const DEFAULT_RELATION_WEIGHT: f64 = 0.5;
27
28/// Default `k` used by `recall` when the caller omits `--k`.
29pub const DEFAULT_K_RECALL: usize = 10;
30
31/// Default `k` for memory KNN searches when the caller omits `--k`.
32pub const K_MEMORIES_DEFAULT: usize = 10;
33
34/// Default `k` for entity KNN searches during graph expansion.
35pub const K_ENTITIES_SEARCH: usize = 5;
36
37/// Default `k` constant used by Reciprocal Rank Fusion in `hybrid-search`.
38pub const RRF_K_DEFAULT: u32 = 60;
39
40/// Maximum result count from the recursive graph CTE in `recall`.
41pub const K_GRAPH_MATCHES_LIMIT: usize = 20;
42
43/// Default `--limit` for `list` in a HUMAN format, when the caller omits it.
44///
45/// Bounds the text rendering only. Under `--format json` an omitted `--limit`
46/// means the whole corpus, because a machine consumer that asked for no ceiling
47/// must not silently receive a page — that asymmetry is the whole of GAP-SG-201.
48///
49/// Declared as 100 until v1.2.7 and referenced by nothing, while `list` carried
50/// a bare `50` in its body: a constant that documented a default the code did
51/// not use is worse than no constant, since it invites a reader to trust it.
52pub const K_LIST_TEXT_DEFAULT_LIMIT: usize = 50;
53
54/// Default `--limit` for `graph entities` when omitted.
55pub const K_GRAPH_ENTITIES_DEFAULT_LIMIT: usize = 50;
56
57/// Default `--limit` for `related` when omitted.
58///
59/// Same value as [`DEFAULT_K_RECALL`], which `related` used until v1.2.7 — a
60/// borrowed name that tied this command's default to `recall`'s `-k` by accident
61/// rather than by intent. Tuning one would silently have moved the other.
62pub const K_RELATED_DEFAULT_LIMIT: usize = 10;
63
64/// Default `--limit` for `history` when omitted.
65pub const K_HISTORY_DEFAULT_LIMIT: usize = 20;
66
67/// Maximum edges pulled when `deep-research` expands the graph around its hits.
68pub const K_DEEP_RESEARCH_GRAPH_EDGES_LIMIT: usize = 50;
69
70/// Default weight for the vector contribution in the `hybrid-search` RRF formula.
71pub const WEIGHT_VEC_DEFAULT: f64 = 1.0;
72
73/// Default weight for the BM25 text contribution in the `hybrid-search` RRF formula.
74pub const WEIGHT_FTS_DEFAULT: f64 = 1.0;
75
76/// GAP-SG-142: envelope members searched, in order, for the primary result
77/// array reshaped by [`crate::agent_surface`].
78///
79/// The list is ordered from most to least specific so an envelope that carries
80/// several arrays (for example `recall`, which exposes `direct_matches`,
81/// `graph_matches` and the merged `results`) is reshaped on the member callers
82/// actually consume. A payload matching none of these falls back to its first
83/// array member.
84///
85/// `nodes` precedes `entities` because the `graph` envelope carries both and
86/// `nodes` is the canonical one there; `entities` is its v1.0.66 alias and is
87/// listed in [`AGENT_SURFACE_ALIAS_ARRAYS`]. Reshaping the alias while leaving
88/// the canonical member untouched is precisely the failure that table closes.
89pub const AGENT_SURFACE_RESULT_KEYS: &[&str] = &[
90    "results", "items", "nodes", "entities", "memories", "hits", "rows", "matches", "data",
91];
92
93/// GAP-SG-142: derived result arrays suppressed once the agent-native surface
94/// reshapes their canonical source.
95///
96/// Each entry is `(subcommand, canonical member, members that merely restate
97/// it)`: `list` clones `items` into `memories`, `graph export` clones `nodes`
98/// into `entities`, `recall` publishes `results` as the concatenation of
99/// `direct_matches` and `graph_matches`, and `related` clones `results` into
100/// `related_memories`.
101///
102/// The subcommand is part of the key because "derived" is a property of one
103/// command's envelope, not of a member name. `results` means a concatenation in
104/// `recall` and a clone in `related`, and in `hybrid-search` it means neither:
105/// there `graph_expansion` skips every id already present in `results`, so
106/// `results` and `graph_matches` are DISJOINT and carry different types
107/// (`HybridSearchItem` against `RecallItem`). Matching on the member name alone
108/// deleted a set no other member restated — and one that
109/// `docs/schemas/hybrid-search.schema.json` lists under `required`, so the
110/// suppression produced an envelope invalid against this project's own schema.
111/// `hybrid-search` is absent from this table by construction, which is what
112/// keeps that from happening again.
113///
114/// Suppression only removes members that are actually present, so a declared
115/// member the envelope never carried is a silent no-op and is never reported as
116/// removed.
117///
118/// The surface reshapes exactly one array per envelope, so leaving a genuinely
119/// derived member in place shipped the unfiltered, unsorted, unprojected copy
120/// right next to the shaped one — the redundancy the projection exists to
121/// remove, and a meta record (`sort`, `output_count`) that contradicted half the
122/// payload. Those are therefore dropped whenever a knob is set.
123///
124/// Without any knob the surface is a no-op and nothing is removed, so the public
125/// v1.0.66 alias contract stays intact byte for byte for every existing caller.
126pub const AGENT_SURFACE_ALIAS_ARRAYS: &[(&str, &str, &[&str])] = &[
127    ("list", "items", &["memories"]),
128    ("graph", "nodes", &["entities"]),
129    ("recall", "results", &["direct_matches", "graph_matches"]),
130    ("related", "results", &["related_memories"]),
131];
132
133/// DEFAULT cap on `hybrid-search --with-graph` graph matches.
134///
135/// ACTIVE by default, unlike the `recall` flag of the same name, which defaults
136/// to unbounded. `hybrid-search` had no cap at all: `graph_expansion` walks
137/// outward from the fused results AND from the five entities nearest the query
138/// embedding, then materialises every memory it reaches with a 300-character
139/// snippet each. A `--k 3` query over a dense neighbourhood measured a 1 112 925
140/// byte envelope — the caller asked for three results and got a megabyte.
141///
142/// A finite default is the only honest shape here: the flag caps a set the
143/// caller never sized, so leaving it unbounded means the envelope is bounded by
144/// the graph rather than by the request. 50 keeps a genuinely useful
145/// neighbourhood while holding the envelope in the tens of kilobytes.
146///
147/// Read it through [`hybrid_search_max_graph_results`], never directly.
148pub const DEFAULT_HYBRID_MAX_GRAPH_RESULTS: usize = 50;
149
150/// Graph-match ceiling for `hybrid-search`: the `--max-graph-results` flag, then
151/// XDG `search.hybrid.max_graph_results`, then
152/// [`DEFAULT_HYBRID_MAX_GRAPH_RESULTS`].
153///
154/// `0` disables the cap at either layer, which is how a caller opts back into
155/// the unbounded pre-v1.2.2 envelope. Returns `None` for that case so the
156/// traversal loop can skip the check entirely.
157pub fn hybrid_search_max_graph_results(flag: Option<usize>) -> Option<usize> {
158    let resolved = flag
159        .or_else(|| {
160            crate::config::get_setting("search.hybrid.max_graph_results")
161                .ok()
162                .flatten()
163                .and_then(|v| v.parse::<usize>().ok())
164        })
165        .unwrap_or(DEFAULT_HYBRID_MAX_GRAPH_RESULTS);
166    (resolved > 0).then_some(resolved)
167}
168
169/// Elements sampled when the surface builds the key vocabulary for a SUGGESTION.
170///
171/// GAP-SG-202: this bounds the suggestion only, never the resolution. Deciding
172/// whether a requested key exists scans every element, because the scan is a
173/// pointer walk per element with no allocation and a wrong `absent` verdict
174/// would refuse a legitimate request. Listing the alternatives is the expensive
175/// half — it collects names — and it only runs once a key has already failed,
176/// so a sample is enough to name a near miss.
177pub const K_VOCABULARY_SAMPLE_ELEMENTS: usize = 64;
178
179/// Hard ceiling on distinct key names collected for a suggestion.
180///
181/// The envelope is caller-influenced, so the collector is a public parser: the
182/// memory rules forbid sizing an allocation from untrusted input without a
183/// ceiling. Reaching it costs a shorter suggestion list, never a refusal.
184pub const K_VOCABULARY_MAX_KEYS: usize = 512;
185
186/// Alternatives named in a refusal message.
187///
188/// Three is what a caller can act on at a glance; a longer list reads as a dump
189/// of the schema rather than as a correction.
190pub const K_VOCABULARY_MAX_SUGGESTIONS: usize = 3;
191
192/// Jaro-Winkler similarity below which a candidate is not offered as a fix.
193///
194/// Jaro-Winkler rather than plain edit distance because it rewards a shared
195/// prefix, and a mistyped key name almost always keeps its prefix — `body_length`
196/// against `body`, `entity_type` against `entity`.
197pub const VOCABULARY_SUGGESTION_MIN_SIMILARITY: f64 = 0.6;
198
199/// Default cap on emitted result elements (`--max-items`). `0` means no cap,
200/// preserving the pre-GAP-SG-142 envelope byte for byte.
201pub const DEFAULT_AGENT_SURFACE_MAX_ITEMS: usize = 0;
202
203/// Default cap on string length in characters (`--truncate-content`).
204/// `0` disables content truncation.
205pub const DEFAULT_AGENT_SURFACE_TRUNCATE_CONTENT: usize = 0;
206
207/// Default cap on the serialized envelope in bytes (`--max-output-bytes`).
208/// `0` disables the ceiling.
209pub const DEFAULT_AGENT_SURFACE_MAX_OUTPUT_BYTES: usize = 0;
210
211/// Inclusive upper bound for `-k`/`--k` on every retrieval command.
212///
213/// Kept at the historical `sqlite-vec` knn ceiling so the message an operator
214/// gets does not change: values above it used to surface a leaky engine error
215/// (`k value in knn query too large, provided 10000 and the limit is 4096`).
216pub const K_QUERY_RANGE_MAX: usize = 4_096;
217
218/// Inclusive upper bound for `--limit` on commands that page over stored rows.
219///
220/// Separate from [`K_QUERY_RANGE_MAX`] because `export --limit` ships a default
221/// of 100_000, so the retrieval ceiling would be a breaking change there. These
222/// limits reach SQLite as a `LIMIT` clause, where the row count bounds the work
223/// no matter what the operator asks for; the ceiling exists to reject absurd
224/// input at parse time rather than to protect memory.
225pub const K_LIST_LIMIT_MAX: usize = 1_000_000;
226
227/// Inclusive upper bound for `--max-hops` and `--depth` on graph traversal.
228///
229/// The breadth-first walks carry visited sets, so a huge value terminates at
230/// the graph diameter rather than running away. The bound is here to keep the
231/// surface honest, and because a request for more than sixty-four hops is a
232/// typo in every real corpus.
233pub const K_MAX_HOPS_CEILING: u32 = 64;
234
235/// Inclusive upper bound for `deep-research --max-sub-queries`.
236///
237/// Unlike the other ceilings this one guards spend, not memory: each sub-query
238/// is a separate REST round trip, so an unbounded value bills the operator for
239/// an unbounded fan-out.
240pub const K_MAX_SUB_QUERIES_CEILING: usize = 64;