openapi: 3.0.3
info:
  title: QSC API
  version: "1"
  description: |
    Complete API reference for the Quasiris Search Cloud — schema management,
    document feeding, and search.

    ## Authentication

    Pass your token in the `X-QSC-Token` request header:

    ```
    X-QSC-Token: <your-token>
    ```

    Two separate tokens are used depending on the operation:

    | Token | Endpoints |
    |-------|-----------|
    | **Admin token** | Schema (`/api/v1/searchapp/*`) |
    | **Feeding token** | Feeding and scheduling (`/api/v1/data/*`, `/api/v1/job/*`) |

    Search endpoints (`/api/v1/search/*`) require no authentication.

servers:
  - url: https://qsc.quasiris.de
    description: Production
  - url: https://qsc-dev.quasiris.de
    description: Development

security:
  - QscToken: []

tags:
  - name: Schema
    description: Define the field schema of a search app (admin token required)
  - name: Scheduling
    description: Trigger and schedule feeding jobs (feeding token required)
  - name: Documents
    description: Create, update, and delete individual documents (feeding token required)
  - name: Bulk
    description: Send multiple documents in one request (feeding token required)
  - name: Full Feed
    description: Replace the entire index with a new dataset (feeding token required)
  - name: Search
    description: Full-text search with filters, facets, and sorting (no auth required)
  - name: Suggest
    description: Query suggestions and autocomplete (no auth required)

paths:

  # ─── Schema ────────────────────────────────────────────────────────────────

  /api/v1/searchapp/schema/{tenant}/{code}:
    put:
      summary: Update search app schema
      description: >
        Replaces the field schema of a search app. QSC syncs the search
        configuration accordingly. **Requires admin token.**

        After updating the schema call **Schedule feeding job** to re-index
        the data with the new field configuration.
      operationId: updateSearchAppSchema
      tags: [Schema]
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchAppSchema'
            example:
              fields:
                - code: name
                  dataType: string
                  searchMode: balanced
                  weight: 3
                  display: true
                  facet: false
                  sort: false
                  suggest: true
                  navigation: false
                  semantic: false
                - code: brand
                  dataType: string
                  searchMode: balanced
                  weight: 2
                  display: true
                  facet: true
                  sort: false
                  suggest: false
                  navigation: false
                  semantic: false
                - code: description
                  dataType: string
                  searchMode: balanced
                  weight: 1
                  display: true
                  facet: false
                  sort: false
                  suggest: false
                  navigation: false
                  semantic: true
                - code: price
                  dataType: double
                  searchMode: off
                  display: true
                  facet: true
                  sort: true
      responses:
        "200":
          description: Schema updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchAppSchema'
        "400":
          description: Invalid request body
        "401":
          description: Missing or invalid token
        "403":
          description: Token does not have WRITE permission on the tenant
        "404":
          description: Tenant or search app not found
        "500":
          description: Internal server error

  # ─── Scheduling ────────────────────────────────────────────────────────────

  /api/v1/job/{tenant}/{code}/feeding/_schedule:
    put:
      summary: Schedule feeding job
      description: >
        Schedules a feeding job for the search app. **Must be called after every
        schema update** so that QSC re-indexes the data with the new field
        configuration. **Requires feeding token.**
      operationId: scheduleFeedingJob
      tags: [Scheduling]
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
            example: {}
      responses:
        "200":
          description: Feeding job scheduled successfully
        "401":
          description: Missing or invalid token
        "403":
          description: Token does not have WRITE permission on the tenant
        "404":
          description: Tenant or search app not found
        "500":
          description: Internal server error

  # ─── Documents ─────────────────────────────────────────────────────────────

  /api/v1/data/{type}/{tenant}/{code}/{documentId}:
    post:
      summary: Update document
      description: >
        Creates or updates a single document. The body must use the QSC
        header/payload format. **Requires feeding token.**
      operationId: updateDocument
      tags: [Documents]
      parameters:
        - $ref: '#/components/parameters/type'
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
        - $ref: '#/components/parameters/documentId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentEnvelope'
            example:
              header:
                id: product-1
                action: update
              payload:
                name: Running Shoes Pro
                brand: Acme
                description: Lightweight trail running shoes with superior grip.
                price: 129.99
      responses:
        "200":
          description: Document queued successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedingQueue'
        "400":
          description: Invalid payload
        "401":
          description: Missing or invalid token
        "403":
          description: Token does not have WRITE permission
        "404":
          description: Tenant or search app not found

    delete:
      summary: Delete document
      description: Marks a document for deletion from the index. **Requires feeding token.**
      operationId: deleteDocument
      tags: [Documents]
      parameters:
        - $ref: '#/components/parameters/type'
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
        - $ref: '#/components/parameters/documentId'
      responses:
        "200":
          description: Document marked for deletion
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedingQueue'
        "401":
          description: Missing or invalid token
        "403":
          description: Token does not have WRITE permission
        "404":
          description: Document, tenant, or search app not found

  /api/v1/data/{tenant}/{code}/{documentId}:
    get:
      summary: Get document
      description: Returns the current indexed state of a document. **Requires feeding token.**
      operationId: getDocument
      tags: [Documents]
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
        - $ref: '#/components/parameters/documentId'
      responses:
        "200":
          description: Document found
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: true
              example:
                - name: Running Shoes Pro
                  brand: Acme
                  description: Lightweight trail running shoes with superior grip.
                  price: 129.99
        "401":
          description: Missing or invalid token
        "404":
          description: Document not found


  # ─── Bulk ──────────────────────────────────────────────────────────────────

  /api/v1/data/bulk/{type}/{tenant}/{code}:
    post:
      summary: Bulk update documents
      description: >
        Creates or updates multiple documents in a single request using a JSON
        array. Each entry must contain a `header` (with `id` and `action`) and
        a `payload` (the document fields). **Requires feeding token.**
      operationId: bulkUpdateDocuments
      tags: [Bulk]
      parameters:
        - $ref: '#/components/parameters/type'
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: '#/components/schemas/DocumentEnvelope'
            example:
              - header:
                  id: product-1
                  action: update
                payload:
                  name: Running Shoes Pro
                  brand: Acme
                  description: Lightweight trail running shoes.
                  price: 129.99
              - header:
                  id: product-2
                  action: update
                payload:
                  name: Trail Boots X
                  brand: Acme
                  description: Durable boots for rough terrain.
                  price: 89.99
              - header:
                  id: product-3
                  action: update
                payload:
                  name: City Sneaker
                  brand: Stride
                  description: Comfortable everyday sneaker.
                  price: 59.99
      responses:
        "200":
          description: Bulk result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkFeedingResponse'
        "400":
          description: Invalid payload
        "401":
          description: Missing or invalid token
        "403":
          description: Token does not have WRITE permission

  /api/v1/data/upload/{type}/{tenant}/{code}:
    post:
      summary: Upload documents from file
      description: >
        Uploads a file of documents for bulk indexing. The file must contain a
        JSON array using the header/payload format. **Requires feeding token.**
      operationId: uploadDocuments
      tags: [Bulk]
      parameters:
        - $ref: '#/components/parameters/type'
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Upload result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkFeedingResponse'
        "400":
          description: Invalid file or unsupported type
        "401":
          description: Missing or invalid token

  # ─── Full Feed ─────────────────────────────────────────────────────────────

  /api/v1/data/fullfeed/start/{tenant}/{code}:
    post:
      summary: Start full feed
      description: >
        Begins a full-feed cycle. Send all documents via bulk or single-document
        endpoints, then call **End full feed** to swap the index.
        Documents not sent since Start are removed when the cycle ends.
        **Requires feeding token.**
      operationId: startFullFeed
      tags: [Full Feed]
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
            example: {}
      responses:
        "200":
          description: Full feed started
        "401":
          description: Missing or invalid token
        "409":
          description: A full feed is already in progress

  /api/v1/data/fullfeed/end/{tenant}/{code}:
    post:
      summary: End full feed
      description: >
        Finalises the full-feed cycle and swaps the index. Documents not sent
        since **Start full feed** are removed from the live index.
        **Requires feeding token.**
      operationId: endFullFeed
      tags: [Full Feed]
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
            example: {}
      responses:
        "200":
          description: Full feed ended and index swap triggered
        "401":
          description: Missing or invalid token
        "404":
          description: No active full feed found

  /api/v1/data/fullfeed/cancel/{tenant}/{code}:
    post:
      summary: Cancel full feed
      description: >
        Aborts the current full-feed cycle without modifying the live index.
        **Requires feeding token.**
      operationId: cancelFullFeed
      tags: [Full Feed]
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      responses:
        "200":
          description: Full feed cancelled
        "401":
          description: Missing or invalid token
        "404":
          description: No active full feed found

  # ─── Search ────────────────────────────────────────────────────────────────

  /api/v1/search/{tenant}/{code}:
    get:
      summary: Search (GET)
      description: >
        Execute a search using query parameters. No authentication required.

        To filter by a facet, add a query parameter in the form `f.{filterId}=value`
        (e.g. `f.brand=Acme`). The filter ID must match a facet configured in the schema.
        Multiple values can be repeated: `f.brand=Acme&f.brand=Stride`.
      operationId: searchGET
      tags: [Search]
      security: []
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
        - $ref: '#/components/parameters/q'
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/rows'
        - $ref: '#/components/parameters/sort'
        - $ref: '#/components/parameters/userId'
        - $ref: '#/components/parameters/sessionId'
        - $ref: '#/components/parameters/requestId'
        - $ref: '#/components/parameters/requestOrigin'
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        "400":
          description: Invalid request
        "404":
          description: Tenant or search app not found

    post:
      summary: Search (POST)
      description: Execute a search using a JSON body. No authentication required.
      operationId: searchPOST
      tags: [Search]
      security: []
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchQuery'
            examples:
              simple:
                summary: Simple keyword search
                value:
                  q: shoes
                  page: 1
                  rows: 10
              withFiltersAndSort:
                summary: Search with filter and sort
                value:
                  q: shoes
                  page: 1
                  rows: 10
                  sort:
                    sort: pricedesc
                  filters:
                    brand:
                      values:
                        - Acme
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        "400":
          description: Invalid request
        "404":
          description: Tenant or search app not found

  # ─── Suggest ───────────────────────────────────────────────────────────────

  /api/v1/search/suggest/{tenant}/{code}:
    get:
      summary: Suggest (GET)
      description: Returns autocomplete suggestions for a partial query. No authentication required.
      operationId: suggestGET
      tags: [Suggest]
      security: []
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
        - $ref: '#/components/parameters/q'
        - $ref: '#/components/parameters/rows'
      responses:
        "200":
          description: Suggestions
          content:
            application/json:
              schema:
                type: object

    post:
      summary: Suggest (POST)
      description: Returns autocomplete suggestions using a JSON body. No authentication required.
      operationId: suggestPOST
      tags: [Suggest]
      security: []
      parameters:
        - $ref: '#/components/parameters/tenant'
        - $ref: '#/components/parameters/code'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchQuery'
            example:
              q: runn
              rows: 5
      responses:
        "200":
          description: Suggestions
          content:
            application/json:
              schema:
                type: object

components:

  securitySchemes:
    QscToken:
      type: apiKey
      in: header
      name: X-QSC-Token
      description: >
        Static API token passed as `X-QSC-Token: <your-token>`.
        Schema endpoints require the **admin token**;
        feeding and scheduling endpoints require the **feeding token**.

  parameters:
    tenant:
      name: tenant
      in: path
      required: true
      description: Tenant code (e.g. `demo`)
      schema:
        type: string
        example: demo
    code:
      name: code
      in: path
      required: true
      description: Search app code (e.g. `trendware`)
      schema:
        type: string
        example: trendware
    type:
      name: type
      in: path
      required: true
      description: "Document format type. Use `qsc` for the standard QSC JSON format."
      schema:
        type: string
        enum: [qsc, reporting]
        example: qsc
    documentId:
      name: documentId
      in: path
      required: true
      description: Unique document identifier
      schema:
        type: string
        example: product-1
    q:
      name: q
      in: query
      required: false
      description: Search query string
      schema:
        type: string
        example: running shoes
    page:
      name: page
      in: query
      required: false
      description: Page number (1-based)
      schema:
        type: integer
        default: 1
        example: 1
    rows:
      name: rows
      in: query
      required: false
      description: Number of results per page
      schema:
        type: integer
        default: 10
        example: 10
    sort:
      name: sort
      in: query
      required: false
      description: Sort identifier configured in the search app (e.g. `pricedesc`)
      schema:
        type: string
        example: pricedesc
    userId:
      name: userId
      in: query
      required: false
      description: User identifier for personalisation and tracking
      schema:
        type: string
    sessionId:
      name: sessionId
      in: query
      required: false
      description: Session identifier for tracking
      schema:
        type: string
    requestId:
      name: requestId
      in: query
      required: false
      description: Custom request identifier echoed back in the response
      schema:
        type: string
    requestOrigin:
      name: requestOrigin
      in: query
      required: false
      description: Origin of the request (e.g. `web`, `app`, `api`)
      schema:
        type: string

  schemas:

    # ── Schema ─────────────────────────────────────────────────────────────

    SearchAppSchema:
      type: object
      required: [fields]
      properties:
        fields:
          type: array
          items:
            $ref: '#/components/schemas/SearchAppSchemaField'

    SearchAppSchemaField:
      type: object
      required: [code, dataType]
      properties:
        code:
          type: string
          description: Field name — must match the key in your documents
          example: name
        dataType:
          $ref: '#/components/schemas/FieldDataType'
        searchMode:
          $ref: '#/components/schemas/SearchMode'
        weight:
          type: integer
          default: 1
          description: Relevance boost — higher means more relevant
          example: 3
        display:
          type: boolean
          default: false
          description: Return this field in search results
        facet:
          type: boolean
          default: false
          description: Enable filtering/faceting by this field
        sort:
          type: boolean
          default: false
          description: Enable sorting by this field
        suggest:
          type: boolean
          default: false
          description: Include this field in search suggestions
        navigation:
          type: boolean
          default: false
          description: Use this field for navigation
        semantic:
          type: boolean
          default: false
          description: Enable semantic / vector search for this field

    FieldDataType:
      type: string
      description: |
        Data type of the field:
        - `string` – Text with full-text analysis
        - `long` – Integer number
        - `double` – Floating point number
        - `boolean` – Boolean (`true`/`false`)
        - `date` – ISO 8601 date
        - `categories` – Hierarchical category tree
        - `attributes` – Key/value attribute pairs
        - `raw` – Unanalyzed raw value
      enum: [string, long, double, boolean, date, categories, attributes, raw]
      example: string

    SearchMode:
      type: string
      description: |
        How the field is searched:
        - `balanced` – Standard full-text search (default for string fields)
        - `fuzzy` – Fuzzy/ngram — tolerates typos and partial matches
        - `exact` – Exact phrase matching
        - `custom` – Preserves manually configured search settings
        - `off` – Field is not searched

        Non-string types (`long`, `double`, `boolean`, `date`) only support
        `exact`, `custom`, and `off`.
      enum: [balanced, fuzzy, exact, custom, off]
      default: balanced
      example: balanced

    # ── Feeding ────────────────────────────────────────────────────────────

    DocumentHeaderUpdate:
      type: object
      required: [id, action]
      properties:
        id:
          type: string
          description: Unique document identifier
          example: product-1
        action:
          type: string
          enum: [update]
          example: update

    DocumentHeaderDelete:
      type: object
      required: [id, action]
      properties:
        id:
          type: string
          description: Unique document identifier
          example: product-1
        action:
          type: string
          enum: [delete]
          example: delete

    DocumentEnvelope:
      oneOf:
        - type: object
          title: Update
          required: [header, payload]
          properties:
            header:
              $ref: '#/components/schemas/DocumentHeaderUpdate'
            payload:
              type: object
              additionalProperties: true
              description: Document fields — keys must match the schema field codes
              example:
                name: Running Shoes Pro
                brand: Acme
                description: Lightweight trail running shoes with superior grip.
                price: 129.99
        - type: object
          title: Delete
          required: [header]
          properties:
            header:
              $ref: '#/components/schemas/DocumentHeaderDelete'

    FeedingQueue:
      type: object
      properties:
        id:
          type: integer
        documentId:
          type: string
          example: product-1
        operation:
          type: string
          enum: [update, delete]
        state:
          type: string
          enum: [CREATED, PREPARED, STARTED, FINISHED, ERROR, IGNORED, CANCELED]
        type:
          type: string
        createdAt:
          type: string
          format: date-time
        finishedAt:
          type: string
          format: date-time

    BulkFeedingResponse:
      type: object
      properties:
        message:
          type: string
          example: OK
        statusCode:
          type: integer
          example: 200
        processedValidDocs:
          type: boolean
        documents:
          type: array
          items:
            type: object
            properties:
              documentId:
                type: string
              state:
                type: string

    # ── Search ─────────────────────────────────────────────────────────────

    SearchQuery:
      type: object
      properties:
        q:
          type: string
          description: Search query string
          example: shoes
        page:
          type: integer
          description: Page number (1-based)
          default: 1
          example: 1
        rows:
          type: integer
          description: Number of results per page
          default: 10
          example: 10
        sort:
          $ref: '#/components/schemas/SortDTO'
        filters:
          type: object
          description: >
            Filters keyed by the facet ID configured in the schema.
            Each value is a Filter object.
          additionalProperties:
            $ref: '#/components/schemas/Filter'
          example:
            brand:
              values: [Acme]
        userId:
          type: string
          description: User identifier for personalisation and tracking
        sessionId:
          type: string
          description: Session identifier for tracking
        requestId:
          type: string
          description: Custom request identifier echoed back in the response
        requestOrigin:
          type: string
          description: Origin of the request (e.g. `web`, `app`)

    SortDTO:
      type: object
      description: >
        Sorting configuration. Only the `sort` field is used — it must be a
        sort ID pre-configured in the search app (e.g. `pricedesc`).
        The `field` and `direction` properties are not supported and are ignored.
      properties:
        sort:
          type: string
          description: Sort ID configured in the search app
          example: pricedesc

    Filter:
      type: object
      example:
        filterType: term
        filterOperator: or
        values:
          - Acme
      properties:
        filterType:
          type: string
          enum: [term, range, slider, tree]
          description: Type of filter
        filterDataType:
          type: string
          enum: [string, number, date]
          description: Data type of the filter values
        filterOperator:
          type: string
          enum: [or, and, not]
          default: or
          description: How multiple values are combined
        values:
          type: array
          description: Values for term filters
          items:
            type: string
          example: [Acme, Stride]
        minValue:
          description: Lower bound for range filters
          oneOf:
            - type: number
            - type: string
        maxValue:
          description: Upper bound for range filters
          oneOf:
            - type: number
            - type: string

    SearchResponse:
      type: object
      properties:
        statusCode:
          type: integer
          example: 200
        requestId:
          type: string
        time:
          type: integer
          description: Total response time in milliseconds
          example: 42
        result:
          $ref: '#/components/schemas/SearchResult'

    SearchResult:
      type: object
      properties:
        total:
          type: integer
          description: Total number of matching documents
          example: 42
        time:
          type: integer
          description: Search execution time in milliseconds
          example: 15
        documents:
          type: array
          items:
            $ref: '#/components/schemas/Document'
        facets:
          type: array
          items:
            $ref: '#/components/schemas/Facet'
        paging:
          $ref: '#/components/schemas/Paging'

    Document:
      type: object
      properties:
        id:
          type: string
          example: product-1
        position:
          type: integer
          example: 1
        document:
          type: object
          additionalProperties: true
          description: Document fields as key-value pairs
          example:
            name: Running Shoes Pro
            brand: Acme
            description: Lightweight trail running shoes with superior grip.
            price: 129.99

    Facet:
      type: object
      properties:
        id:
          type: string
          example: brand
        name:
          type: string
          example: Brand
        values:
          type: array
          items:
            type: object
            properties:
              value:
                type: string
                example: Acme
              count:
                type: integer
                example: 12

    Paging:
      type: object
      properties:
        page:
          type: integer
          example: 1
        rows:
          type: integer
          example: 10
        total:
          type: integer
          example: 42
        totalPages:
          type: integer
          example: 5
