Hybrid Search & Ranking Engine
SemSearch implements a hybrid retrieval and ranking pipeline in SearchService (semsearch.services.searcher) that combines multimodal vector similarity with SQLite FTS5 lexical matching, modality-aware Z-score calibration, and symbol-aware code resolution.
1. Dual-Query Routing & Task Prompts
Every query is first inspected for target index routing:
- General Search (
index.sqlite): Default mode for documents, PDFs, images, screenshots, audio, and video. - Source Code Search (
code.sqlite): Activated automatically when the query begins with thecode:prefix (e.g.,code: z_score_normalize).
To align with asymmetric retrieval models like embeddinggemma-2, SemSearch formats text inputs with task-specific prefixes before calling Ollama's /api/embed endpoint:
| Operation | Prompt Template |
|---|---|
| General Search Query | task: search result \| query: {query} |
| Source Code Query | task: code retrieval \| query: {query} |
| Document / Code Segment Indexing | title: {title} \| text: {segment_text} |
2. Modality-Aware Z-Score Normalization
Raw cosine similarity distributions differ substantially across modalities—a text-to-image or text-to-audio cosine score is naturally lower and narrower than a text-to-text cosine score even for an exact conceptual match. Comparing raw cosines directly would cause text documents to drown out images, screenshots, and audio clips.
SemSearch solves this in semsearch.core.vectors.z_score_normalize() by calibrating scores per modality (Modality.TEXT, Modality.IMAGE, Modality.AUDIO):
- Cosine Similarity: All stored
float16segment vectors are loaded into a contiguous matrix and compared against the query vector infloat32: $\(\cos(\mathbf{q}, \mathbf{d}_i) = \frac{\mathbf{q} \cdot \mathbf{d}_i}{\|\mathbf{q}\|_2 \|\mathbf{d}_i\|_2}\)$ - Modality Centering (10% Trimmed Mean): Scale-eligible segments (
sets_scale = True, i.e.,SegmentKind != NAME) are grouped by modality. If a modality bucket has at least 20 scale-eligible segments, its baseline mean \(\mu_m\) is computed using a symmetric 10% trimmed mean (fallback: global trimmed mean across all scale-eligible segments): $\(c_i = \cos(\mathbf{q}, \mathbf{d}_i) - \mu_{m(i)}\)$ - Background Dispersion (\(\sigma\)): To prevent genuine top hits from inflating the standard deviation, the top 15% highest centered scale-eligible scores are excluded when computing the background variance (when \(\ge 10\) scale-eligible segments exist), with a minimum floor of \(\sigma \ge 0.045\): $\(z_i = \frac{c_i}{\max\left(\sqrt{\frac{1}{|B|}\sum_{j \in B} c_j^2},\; 0.045\right)}\)$
- File-Level Aggregation: Each file retains the maximum \(z\)-score and corresponding best segment across all of its indexed segments.
3. Lexical FTS5 Search & Query Parsing
In parallel with vector search, semsearch.core.search_text parses the query for lexical retrieval:
- Kind Hints (
extract_kind_hints): Natural-language category words in the query (such aspdf,pdfs,screenshot,screenshots,photo,image,audio,podcast,video,doc,document) are recognized asFileKindhints. If a query consists solely of category words (e.g.,"screenshots"), SemSearch executes a pure kind filter. - Stopword & Term Extraction (
extract_terms): Extracts meaningful search tokens while stripping conversational filler words. - Identifier Detection (
extract_identifier): RecognizescamelCase,PascalCase,snake_case, anddotted.identifiersin code queries to construct exact phrase matches. - Automatic Prefix Expansion: For terms with \(\ge 3\) characters that do not appear verbatim in the FTS vocabulary (
db.is_indexed(term)), SemSearch automatically appends a prefix wildcard (term*) so partial words still match as you type. - Term Coverage (
compute_coverage): Computes the fraction of query terms present in each candidate file (\(\text{cov} \in [0.0, 1.0]\)).
4. Quality Gating & Score Fusion
A file is admitted to the candidate set if and only if it passes at least one quality gate:
- Semantic Gate: Best segment Z-score \(z \ge 2.2\) (
SEMANTIC_Z_GATE), or - Lexical Gate: Query term coverage \(\text{cov} \ge 0.5\) (at least half of the query terms appear in the file), or
- Pure Kind Query: The user searched purely for a file category (e.g.,
"pdfs").
Hybrid Scoring Formula
For every candidate file \(f\), the fused score is computed as:
| Component | Value | Description |
|---|---|---|
| \(w_{\text{meaning}}\) | 0.5 (lookup) / 1.0 (semantic) |
Weight applied to the modality-normalized Z-score. |
| \(w_{\text{words}}\) | 4.0 (general lookup) / 2.0 (general semantic or code lookup) / 1.0 (code semantic) |
Weight applied to non-linear lexical coverage \((\text{cov}_f)^{1.5}\). |
| \(B_{\text{kind}}\) | +1.0 |
Applied when the file's FileKind matches a category hint in the query. |
| \(B_{\text{name}}\) | Up to +3.0 |
Applied when \(\text{cov}_f \ge 0.5\) and query terms match the diacritic-folded filename stem: \(3.0 \times (\text{match\_ratio})^{1.5}\). |
| \(B_{\text{def}}\) | +3.0 (strong) / +1.5 (weak) |
Applied in code search when find_definition() locates a symbol definition (def, class, fn, struct, interface, etc.) matching the query. |
5. Symbol Line Resolution & Result Folding
- Code Line Jump (
_resolve_code_place): For code hits, SemSearch inspects matching segments (and falls back to scanning the source file) using language-aware definition patterns (semsearch.core.code_lookup.find_definition). It setshit.focusto the exact 1-based line number of the function, class, or variable definition so opening the result jumps your IDE cursor directly to that line. - Primary vs. Weaker Folding (
ResultSet): Once all hits are sorted by(score, mtime)descending, hits with \(\text{score} \ge 0.5 \times \text{top\_score}\) are placed inresult_set.ranked(up tolimit = 50), while lower-scoring hits are separated intoresult_set.weakerto keep the primary UI clean and high-signal.