> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# hybrid_search

> Search the workspace using RRF fusion over lexical and vector legs.

**Cost class: medium.**

The workspace's one search method.  Combines BM25 keyword matching with
dense-vector semantic similarity via Reciprocal Rank Fusion (RRF), so it
handles both exact-identifier queries (ticker symbols, account codes,
clause numbers) and paraphrase/concept queries in a single call.  The
retrieval configuration is fixed server-side at the evaluated-best setting
(see ``service.SEARCH_LEGS``) — there are no per-request retrieval knobs to
tune, and no single-leg variants to choose between.

``path_prefix`` omitted means the whole workspace — there is no ``all``
sentinel to get wrong.



## OpenAPI

````yaml /openapi.json post /v1/workspaces/{workspace_id}/tools/hybrid-search
openapi: 3.1.0
info:
  description: >-
    Document intelligence: parse, split, classify, extract, and ground files, or
    build a searchable workspace. Authenticate with ``X-API-Key``.
  title: NDI Platform API
  version: 0.1.0
servers:
  - url: https://ndi-api.nace.ai
security: []
tags: []
paths:
  /v1/workspaces/{workspace_id}/tools/hybrid-search:
    post:
      tags:
        - tools
      summary: hybrid_search
      description: >-
        Search the workspace using RRF fusion over lexical and vector legs.


        **Cost class: medium.**


        The workspace's one search method.  Combines BM25 keyword matching with

        dense-vector semantic similarity via Reciprocal Rank Fusion (RRF), so it

        handles both exact-identifier queries (ticker symbols, account codes,

        clause numbers) and paraphrase/concept queries in a single call.  The

        retrieval configuration is fixed server-side at the evaluated-best
        setting

        (see ``service.SEARCH_LEGS``) — there are no per-request retrieval knobs
        to

        tune, and no single-leg variants to choose between.


        ``path_prefix`` omitted means the whole workspace — there is no ``all``

        sentinel to get wrong.
      operationId: hybrid_search_v1_workspaces__workspace_id__tools_hybrid_search_post
      parameters:
        - description: Workspace identifier.
          in: path
          name: workspace_id
          required: true
          schema:
            description: Workspace identifier.
            format: uuid
            title: Workspace Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
          description: Successful Response
        '504':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Gateway Timeout
        default:
          content:
            application/json:
              example:
                error:
                  code: invalid_request
                  detail: null
                  message: >-
                    Request body has extra fields that this operation does not
                    accept.
                  request_id: req-01j9k2n3p4q5r6s7t8v9
                  retryable: false
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: >-
            Typed error envelope used by every 4xx and 5xx. ``error.code``
            distinguishes the failure; HTTP status is a consequence of the code.
      security:
        - APIKeyHeader: []
components:
  schemas:
    SearchRequest:
      additionalProperties: false
      description: |-
        Request for ``hybrid-search``, the workspace's one search method.

        The retrieval configuration (legs, fusion) is fixed server-side — see
        ``tools.service.SEARCH_LEGS`` — so the body carries only the query and
        scope. ``path_prefix`` and ``categories`` are additive scope filters —
        omitting them means the whole workspace.
      properties:
        categories:
          anyOf:
            - items:
                type: string
              type: array
            - type: 'null'
          title: Categories
        k:
          default: 10
          maximum: 100
          minimum: 1
          title: K
          type: integer
        path_prefix:
          anyOf:
            - type: string
            - type: 'null'
          title: Path Prefix
        query:
          maxLength: 4096
          minLength: 1
          title: Query
          type: string
      required:
        - query
      title: SearchRequest
      type: object
    SearchResponse:
      additionalProperties: false
      description: >-
        Response of ``hybrid-search``.


        ``candidates_seen`` is the pre-access-filter count so the caller can
        tell

        when truncation happened upstream of the access gate.


        Every ``hits[].path`` is an uploaded **source** path. Each hit also
        carries the

        ingestion derivative it was actually read from, but those fields are
        internal

        to the host (the agent tools address files that way) and are excluded
        from this

        body — a ``/v1`` caller only ever sees the files it uploaded.
      properties:
        candidates_seen:
          title: Candidates Seen
          type: integer
        coverage:
          $ref: '#/components/schemas/Coverage'
        hits:
          items:
            $ref: '#/components/schemas/Evidence'
          title: Hits
          type: array
        truncated:
          title: Truncated
          type: boolean
      required:
        - hits
        - candidates_seen
        - truncated
        - coverage
      title: SearchResponse
      type: object
    ErrorEnvelope:
      description: Every non-2xx body on ``/v1``.
      example:
        error:
          code: invalid_request
          detail: null
          message: Request body has extra fields that this operation does not accept.
          request_id: req-01j9k2n3p4q5r6s7t8v9
          retryable: false
      properties:
        error:
          $ref: '#/components/schemas/Error'
      required:
        - error
      title: ErrorEnvelope
      type: object
    Coverage:
      additionalProperties: false
      description: >-
        What the access gate withheld, in counts and label names only.


        Never a name, path, or fragment of withheld content. Naming the labels

        leaks nothing the caller cannot already read from the workspace config,
        and

        it turns "some results were withheld" into an actionable request.
      properties:
        labels_required:
          description: Which labels would have been needed to see the withheld candidates.
          items:
            type: string
          title: Labels Required
          type: array
        request_access_hint:
          anyOf:
            - type: string
            - type: 'null'
          title: Request Access Hint
        restricted_candidates:
          default: 0
          title: Restricted Candidates
          type: integer
      title: Coverage
      type: object
    Evidence:
      additionalProperties: false
      description: >-
        The citation primitive.


        Every result that asserts something about a document or the corpus
        carries

        it. A synthesised sentence with no evidence behind it is a bug, not a

        degraded mode.


        ``path`` is always the uploaded **source** path — that is what every
        external

        audience (``/v1`` bodies, the console, the search-agent result) is
        shown. The

        two ``exclude=True`` fields below are the host's internal twin of it,
        for the

        audience that must not see source paths at all (the agent's tools
        address

        ingestion derivatives); they are never serialized, so the ``/v1`` wire
        shape

        is exactly what it was before they existed.


        ``component`` closes the loop from a hit back to the file tools: it is
        the

        public component name of the hit — exactly the vocabulary
        ``file-metadata``

        lists and ``qa-file`` / ``read-file`` accept as ``component=`` (the tab
        name

        for a spreadsheet, the section/subcategory for a split document).
        ``None``

        means a single-component file or that attribution was unavailable.


        ``locator`` is present only when the producer actually located the
        snippet.

        ``None`` is not a degraded mode: an engine that answered from a whole

        component (the QA agent's quotes, the DuckDB agent's SQL receipts)
        asserts

        the file and ``component``, nothing finer. Pinning such a snippet to a

        region — a page bbox, a sheet cell range — is a separate ``ground`` call
        on

        ``path`` with ``snippet`` as the target text (and ``component`` as the

        ``sheet`` hint when the file is a spreadsheet).
      properties:
        component:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Public component of the hit, in the vocabulary file-metadata lists
            and qa-file/read-file accept as component= (tab name for
            spreadsheets, section for split documents); None for a
            single-component file or when attribution is unavailable.
          title: Component
        file_id:
          format: uuid
          title: File Id
          type: string
        locator:
          anyOf:
            - discriminator:
                mapping:
                  audio_range:
                    $ref: '#/components/schemas/AudioRange'
                  json_pointer:
                    $ref: '#/components/schemas/JsonPointer'
                  jsonl_record:
                    $ref: '#/components/schemas/JsonlRecord'
                  row_range:
                    $ref: '#/components/schemas/RowRange'
                  spreadsheet_range:
                    $ref: '#/components/schemas/SpreadsheetRange'
                  text_range:
                    $ref: '#/components/schemas/TextRange'
                  visual_region:
                    $ref: '#/components/schemas/VisualRegion'
                propertyName: kind
              oneOf:
                - $ref: '#/components/schemas/TextRange'
                - $ref: '#/components/schemas/VisualRegion'
                - $ref: '#/components/schemas/SpreadsheetRange'
                - $ref: '#/components/schemas/JsonlRecord'
                - $ref: '#/components/schemas/JsonPointer'
                - $ref: '#/components/schemas/AudioRange'
                - $ref: '#/components/schemas/RowRange'
            - type: 'null'
          description: >-
            Where the snippet sits, when the producer located it. None means the
            citation is component-level: ground the snippet against path
            (sheet=component for spreadsheets) to place it.
          title: Locator
        page:
          anyOf:
            - type: integer
            - type: 'null'
          description: >-
            The 1-based page the producer attributed the snippet to when it
            produced no locator; None when unknown or when the locator already
            carries the page.
          title: Page
        path:
          title: Path
          type: string
        relevance_score:
          title: Relevance Score
          type: number
        snippet:
          title: Snippet
          type: string
        why:
          description: Which retrieval leg matched, or which graph path led here.
          title: Why
          type: string
      required:
        - file_id
        - path
        - relevance_score
        - snippet
        - why
      title: Evidence
      type: object
    Error:
      description: The error object, per spec 12 §1.8.
      properties:
        code:
          $ref: '#/components/schemas/ErrorCode'
        detail:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          description: Structured payload, when the code carries one.
          title: Detail
        message:
          description: Human- and model-readable; says what to do differently.
          title: Message
          type: string
        request_id:
          description: Per-request correlation id; matches the ``X-Request-Id`` header.
          title: Request Id
          type: string
        retryable:
          description: Whether retrying the identical request could succeed.
          title: Retryable
          type: boolean
      required:
        - code
        - message
        - retryable
        - request_id
      title: Error
      type: object
    AudioRange:
      additionalProperties: false
      properties:
        end_ms:
          title: End Ms
          type: integer
        kind:
          const: audio_range
          default: audio_range
          title: Kind
          type: string
        start_ms:
          title: Start Ms
          type: integer
      required:
        - start_ms
        - end_ms
      title: AudioRange
      type: object
    JsonPointer:
      additionalProperties: false
      description: An RFC 6901 pointer into a JSON source; an empty string means the root.
      properties:
        kind:
          const: json_pointer
          default: json_pointer
          title: Kind
          type: string
        pointer:
          pattern: ^(?:$|/)
          title: Pointer
          type: string
      required:
        - pointer
      title: JsonPointer
      type: object
    JsonlRecord:
      additionalProperties: false
      properties:
        column:
          anyOf:
            - type: string
            - type: 'null'
          title: Column
        kind:
          const: jsonl_record
          default: jsonl_record
          title: Kind
          type: string
        row_offset:
          title: Row Offset
          type: integer
      required:
        - row_offset
      title: JsonlRecord
      type: object
    RowRange:
      additionalProperties: false
      description: A 0-based inclusive row run on a parquet table.
      properties:
        column:
          anyOf:
            - type: string
            - type: 'null'
          title: Column
        end_row:
          minimum: 0
          title: End Row
          type: integer
        kind:
          const: row_range
          default: row_range
          title: Kind
          type: string
        start_row:
          minimum: 0
          title: Start Row
          type: integer
      required:
        - start_row
        - end_row
      title: RowRange
      type: object
    SpreadsheetRange:
      additionalProperties: false
      properties:
        a1_range:
          title: A1 Range
          type: string
        kind:
          const: spreadsheet_range
          default: spreadsheet_range
          title: Kind
          type: string
        sheet:
          title: Sheet
          type: string
      required:
        - sheet
        - a1_range
      title: SpreadsheetRange
      type: object
    TextRange:
      additionalProperties: false
      description: >-
        A character span in a derivative's text, with the page(s) it falls on.


        ``pages`` is every page the span overlaps — a chunk that straddles a
        page

        marker belongs to both, and a reader handed only the first would miss
        the

        part of the span that carries the match. ``page`` is the first of them,

        kept because most producers and consumers only ever deal in one page.
      properties:
        char_end:
          title: Char End
          type: integer
        char_start:
          title: Char Start
          type: integer
        kind:
          const: text_range
          default: text_range
          title: Kind
          type: string
        page:
          anyOf:
            - type: integer
            - type: 'null'
          title: Page
        pages:
          items:
            type: integer
          title: Pages
          type: array
      required:
        - char_start
        - char_end
      title: TextRange
      type: object
    VisualRegion:
      additionalProperties: false
      properties:
        bbox:
          maxItems: 4
          minItems: 4
          prefixItems:
            - type: number
            - type: number
            - type: number
            - type: number
          title: Bbox
          type: array
        kind:
          const: visual_region
          default: visual_region
          title: Kind
          type: string
        page:
          title: Page
          type: integer
      required:
        - page
        - bbox
      title: VisualRegion
      type: object
    ErrorCode:
      description: |-
        Every error the ``/v1`` surface can return.

        Clients must treat this as an **open** enum: a new member is an additive
        change (spec 12 §9), and a client that crashes on an unknown code makes
        every future error a breaking change.
      enum:
        - invalid_request
        - unsupported_file_type
        - file_too_large
        - invalid_schema
        - invalid_sql
        - invalid_path_prefix
        - command_not_permitted
        - batch_too_large
        - domain_validation_failed
        - default_label_required
        - wait_required_for_zero_retention
        - workspace_not_found
        - file_not_found
        - job_not_found
        - upload_not_found
        - node_not_found
        - schema_not_found
        - domain_not_found
        - session_not_found
        - access_label_not_found
        - path_conflict
        - schema_version_conflict
        - access_label_name_taken
        - workspace_name_taken
        - access_label_in_use
        - upload_expired
        - file_not_ingested
        - workspace_deleting
        - workspace_not_deletable
        - ingestion_in_progress
        - job_not_cancellable
        - confirmation_required
        - domain_not_published
        - domain_in_use
        - reserved_domain_slug
        - kg_not_built
        - kg_build_in_progress
        - session_busy
        - range_not_satisfiable
        - result_expired
        - access_denied
        - access_backstop_violation
        - access_mode_not_implemented
        - rate_limited
        - concurrency_limit_reached
        - quota_exceeded
        - corrupt_file
        - zero_byte_file
        - encrypted_file
        - parse_failed
        - ocr_failed
        - conversion_failed
        - sheetless_workbook
        - unplayable_media
        - unclassified_pages
        - job_cancelled
        - job_timeout
        - upstream_unavailable
        - internal_error
        - unauthorized
        - not_implemented
      title: ErrorCode
      type: string
  securitySchemes:
    APIKeyHeader:
      in: header
      name: X-API-Key
      type: apiKey

````