---
title: "Semantic search across knowledge bases"
method: POST
path: "/v1/knowledge-base/search"
tags: ["knowledge-base"]
---

# Semantic search across knowledge bases

`POST /v1/knowledge-base/search`

## Request body

- SearchRequest
  - `query` string, required
  - `kb_ids` string[], required
  - `rerank` boolean — Request semantic reranking of the candidate set. For mixed embedding spaces, the service may still rerank automatically when Cohere is configured because raw vector scores are not comparable across spaces.
  - `limit` integer
  - `prefer_draft` boolean
  - `version_overrides` object, nullable
  - `alpha` number — Hybrid search weighting between dense and sparse vectors. `alpha=1.0` is dense-only, `alpha=0.5` weights both equally. Minimum is 0.1: sparse-dominant queries on this hybrid index starve dense signal without materially improving recall, and pure sparse (`alpha=0.0`) sends a degenerate all-zero dense vector that Pinecone rejects.

## Response `200`

Successful Response

- SearchResponse
  - `results` SearchResult[], required
    - `chunk_id` string, required
    - `kb_id` string, uuid, required
    - `document_id` string, uuid, required
    - `version_id` string
    - `document_title` string, required
    - `source_type` string, required
    - `content` string, required
    - `score` number, required
    - `chunk_index` integer, required
    - `position` TextPosition — Character-offset position for text and markdown chunks. `start_char` and `end_char` are **Unicode code-point** offsets into the decoded UTF-8 source text. In Python: ``text[start_char:end_char]``. JavaScript's ``String.prototype.slice()`` counts UTF-16 code units, which differ from code-point offsets for non-BMP characters (emoji, some CJK). JS consumers should convert with ``Array.from(str)`` before slicing — see the API docs for a helper.
      - `type` 'text'
      - `start_char` integer, required
      - `end_char` integer, required
    - `sensitivity` string, nullable
    - `sensitivity_level` integer, nullable
    - `pii_status` string
    - `pii_sensitivity` string
    - `pii_coverage_status` string
  - `total_results` integer, required — Total candidate count before the flat `results` list is truncated by `limit`.
  - `grouped_results` SearchResultGroup[], nullable — Present only when mixed embedding spaces were searched without reranking. In that case the response is grouped by embedding space instead of pretending there is one globally ranked list.
    - `embedding_model` string, required
    - `embedding_dimensions` integer, required
    - `kb_ids` string[], required
    - `results` SearchResult[], required
      - `chunk_id` string, required
      - `kb_id` string, uuid, required
      - `document_id` string, uuid, required
      - `version_id` string
      - `document_title` string, required
      - `source_type` string, required
      - `content` string, required
      - `score` number, required
      - `chunk_index` integer, required
      - `position` TextPosition — Character-offset position for text and markdown chunks. `start_char` and `end_char` are **Unicode code-point** offsets into the decoded UTF-8 source text. In Python: ``text[start_char:end_char]``. JavaScript's ``String.prototype.slice()`` counts UTF-16 code units, which differ from code-point offsets for non-BMP characters (emoji, some CJK). JS consumers should convert with ``Array.from(str)`` before slicing — see the API docs for a helper.
        - `type` 'text'
        - `start_char` integer, required
        - `end_char` integer, required
      - `sensitivity` string, nullable
      - `sensitivity_level` integer, nullable
      - `pii_status` string
      - `pii_sensitivity` string
      - `pii_coverage_status` string
  - `ranking_mode` 'global_score' | 'reranked' | 'grouped_unranked', required — `global_score` means one embedding space ranked by raw vector score; `reranked` means a text reranker determined final ordering; `grouped_unranked` means mixed embedding spaces were returned as separate groups.
  - `answerability` SearchAnswerability — Curated, consumer-facing slice of the retrieval-confidence verdict (ENG-671). Deliberately a SUBSET of the internal ``AnswerabilityDecision``: the public search API exposes the verdict + why, NOT internal gate config (``mode`` / ``caller`` / ``action`` / ``suppressed`` / thresholds). Those are operational details — and on this endpoint ``action`` is always ``pass`` anyway, so surfacing them would only be noise + needless coupling for API consumers.
    - `label` 'answerable' | 'low_confidence' | 'not_answerable' | 'unknown', required — Retrieval-confidence verdict for the query against the searched KBs.
    - `reason_code` 'no_results' | 'below_low_threshold' | 'weak_band' | 'above_high_threshold' | 'not_actionable_basis' | 'verdict_failed', required — Why the verdict landed where it did.
    - `raw_top_score` number, nullable — Top pre-normalization retrieval score; null on an empty or non-raw result set.
    - `score_basis` 'raw_dotproduct' | 'minmax_norm' | 'cohere_rerank' | 'grouped', required — Retrieval-path regime the score is on: `raw_dotproduct` / `minmax_norm` / `cohere_rerank` / `grouped`. The verdict is only actionable on `raw_dotproduct`.

## Other responses

- `400` — Bad request
- `401` — Authentication required
- `422` — Validation Error

---

[API](https://skmtc.net/oneloop-hq/apis/feather-api.md) · [All operations](https://skmtc.net/oneloop-hq/apis/feather-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/oneloop-hq/feather-api/versions/888bdd5c076e/schema)
