Hybrid Search API

Keyword results and an AI answer, from one call.

One call returns keyword results and starts an AI answer over the same catalog. It is the endpoint behind the Hybrid Search SDK workflow. Below we run one search, then narrow it with facets and filters.

This is the search to use for content. Articles, video and documents bring readers who ask in sentences and want an answer, not only a list. For a product catalog, where traffic is heavy and every query must render at once, use the Search API instead.

Base URL https://api.askmiso.com. Authenticate with the X-API-KEY header, or ?api_key=. See Authentication.

Search (+ answer)

POST /v1/ask/search

Field Type Default Notes
q string required The user's query.
answer boolean | "AUTO" true Run the answer pipeline. "AUTO" decides per query.
rows integer 10 Number of search results.
start integer 0 Result offset.
fl string[] ["title"] Product fields to return (any uploaded field, incl. custom_attributes).
source_fl string[] ["title"] Fields returned for answer sources.
stemming boolean true Reduce query words to their root form.
facets (string | object)[] [] Fields to count, such as tags. Returned in facet_counts. See below.
order_by string | object[] relevance relevance, published_at (newest first), -published_at (oldest first), or your own fields. See below.
fq string Filter, for example tags:Computing. See Filter Syntax (fq).
user_id / anonymous_id string For personalization.
curl -X POST "https://api.askmiso.com/v1/ask/search" \
  -H "X-Api-Key: YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "q": "interest rate cuts 2026",
    "answer": "AUTO",
    "rows": 5,
    "fl": ["title", "url", "cover_image"],
    "fq": "section:\"markets\""
  }'
{
  "message": "success",
  "data": {
    "took": 731,
    "total": 3,
    "question_id": "064bda22-1e30-4411-b735-9370a8e48aa4",
    "miso_id": "85d792d9-50a2-43dc-9…",
    "products": [
      {
        "product_id": "wikinews-2911293",
        "title": "Gregory Kurtzer discusses plans for Rocky Linux with Wikinews…",
        "url": "https://en.wikinews.org/wiki/Gregory_Kurtzer_discusses_plans…"
      }
    ],
    "facet_counts": {
      "facet_fields": {
        "tags": [["Computing", 2], ["FLOSS", 2]]
      }
    },
    "parsed_query": null,
    "suggestion": null
  }
}

The answer is not in this response. You get question_id, and you poll the same endpoint the Ask workflow uses:

POST /v1/ask/search                       -> products now, plus question_id
GET  /v1/ask/questions/{question_id}/answer  -> the answer, when finished

The search returns in about 2 seconds, and the answer follows about 5 seconds later with 7 sources. Render the results first, then fill the answer in when it arrives.

Field What it is
took Search time in milliseconds.
total Matching products, before rows is applied.
products The results. fl decides which fields each one carries.
question_id Poll this for the answer.
facet_counts.facet_fields Counts per value, for each field in facets.
miso_id The request id. Send it back on an interaction to attribute the click.
parsed_query, suggestion Query understanding and a spelling suggestion. null when there is nothing to report.

The search and the answer come back separately. Render the results as soon as they arrive, and let the answer appear when it is ready.

How Miso reads the query

q is a search query, and these operators work on it. The counts below come from a catalog of 20,901 news articles.

You write You get Matched
linux kernel Both words. Miso joins bare words with AND. 8
"linux kernel" The exact phrase, in that order. 5
linux AND kernel The same as the first row. 8
software OR hardware Either word. 438
kernel NOT linux The first word, without the second. 8
comput* Any word that starts with comput. 958

Three details:

  • A bare space means AND. linux kernel and linux AND kernel both return 8. A reader who types more words gets fewer results, not more.
  • NOT subtracts. kernel alone matches 16 articles, 8 of which also say linux, so kernel NOT linux leaves 8.
  • Quotes narrow. "open source" returns 100, the loose form 317.

How Miso repairs a query

A broken quote never fails the search. Miso normalizes the query before it runs:

Input What Miso does
“linux kernel” Curly quotes become straight ones. It matched the same 5 articles as the straight form.
"linux kernel An odd number of quotes: the unmatched one is removed. It matched 8, the same as no quotes.
AT&T, m&a, west-bank A word joined by &, - or @ is quoted for you, so it survives as one term.
CJK text Miso adds the phrase boundaries, because the words carry no spaces.

A malformed query therefore returns results rather than an error.


Order the results

order_by takes a shorthand string, or a list of objects for your own fields. The default is relevance.

Value Order
relevance Best match first. The default.
published_at Newest first.
-published_at Oldest first.

The minus sign does not mean descending here. published_at is already newest first, and -published_at reverses it to oldest first. It reads backwards, so check it against your own data before you ship a sort control.

Against a catalog of 20,901 news articles:

2023-05-01  Microsoft, Nware sign 10-year cloud gaming deal
2023-03-09  Netherlands set to further restrict semiconductor technology exports
2021-07-05  Ransomware attack hits over 200 US companies
2004-11-15  Big Linux Beta 3 released
2004-11-16  Longhorn for 2006, according to Gates
2004-11-29  Lycos launches screensaver to increase spammers' bills

A record with no published_at sorts by updated_at. It is not pushed to the end. In the run above, three undated articles appeared in the middle of the list, each in the position its updated_at earned.

Sort on your own fields

Send a list instead of a string. Each entry names a field, and Miso applies them in order:

{
  "q": "technology",
  "order_by": [
    {
      "field": "custom_attributes.word_count"
    },
    {
      "field": "_search_score"
    }
  ]
}
Key What it does
field Any numeric or boolean field in your catalog, or _search_score, or published_at.
default_value The value to use when a record does not have that field. Default 0.0.
tie_breaker What to sort on when two records tie.

Facet the results

facets returns counts per value, for the products that match the query. Use them to build filters.

The short form is a list of field names:

{
  "q": "technology",
  "facets": ["tags"]
}
{
  "facet_counts": {
    "facet_fields": {
      "tags": [
        ["Science and technology", 1807],
        ["United States", 1010],
        ["North America", 903],
        ["Europe", 484]
      ]
    }
  }
}

Each entry is [value, count], sorted by count, largest first. The default is the 10 most common values.

Control the facet

The long form is an object:

Key What it does
field The field to count. A custom attribute uses its dotted path.
size How many values to return. Default 10.
alias The key to use in the response. Needed when you facet one field twice.
include A regular expression. Keep only values that match.
exclude A regular expression. Drop values that match.
ranges Buckets for a numeric or date field, each with a key.
{
  "facets": [
    {
      "field": "tags",
      "alias": "topics",
      "size": 3
    }
  ]
}
{
  "facet_counts": {
    "facet_fields": {
      "topics": [
        ["Science and technology", 1807],
        ["United States", 1010],
        ["North America", 903]
      ]
    }
  }
}

The alias replaces the field name as the key, so two facets on one field do not collide.

include and exclude are case sensitive, and they match anywhere in the value unless you anchor them:

{
  "facets": [
    {
      "field": "tags",
      "size": 5,
      "include": "Comp.*"
    }
  ]
}

That returns Computing (332), Computron (Wikinewsie) (6), Computer Fraud and Abuse Act (3), and Computron (WWC2013) (2). Neither option changes the search results, only the facet values.

Filter by a facet

A count next to an option is there so a reader can judge the click before they make it. "Computing 48" is worth a click. "Computing 2" is not.

That sets a rule for what happens after the click. Every shop with a filter rail in the sidebar behaves the same way, and readers arrive expecting it:

  1. The results narrow to the value they picked.
  2. Every other group updates. Its counts now describe what is still reachable, so a dead end is visible before they hit it.
  3. The group they picked from does not count its own filter. The other options in it stay judgeable, and the reader can switch or go back.

Point 3 is the one that is easy to get wrong. A group that counts its own filter collapses its other options to the overlap with the current choice, and a reader loses both the comparison and the way back.

Miso does this for you. Send the selection in facet_filters, keyed by the field the facet reads:

{
  "q": "linux",
  "facets": [
    { "field": "tags", "size": 4 },
    { "field": "custom_attributes.word_count",
      "ranges": [{ "to": 200, "key": "short" },
                 { "from": 200, "to": 600, "key": "medium" },
                 { "from": 600, "key": "long" }] }
  ],
  "facet_filters": {
    "tags": { "terms": ["Computing"] }
  }
}

The rule is one line. Each group counts with every filter applied except its own. Two groups over the same catalog of 78 articles, one on tags and one on custom_attributes.word_count:

Ticked Topic counts Length counts Results
Nothing 48, 46, 46, 43 42, 22, 14 78
Topic Computing 48, 46, 46, 43 27, 12, 9 48
Length long 9, 8, 6, 5 42, 22, 14 14
Both 9, 8, 6, 5 27, 12, 9 9

The last row is the rule at work. Topic reads 9, 8, 6, 5, which is the long row, because Topic applies Length and skips itself. Length reads 27, 12, 9, which is the Computing row, for the same reason in reverse.

That is what makes the panel usable. With both ticked, the results are 9, and Topic still offers FLOSS at 8. The reader can see the size of the swap before they make it. Neither request names a facet to leave alone. The key on the filter does that.

A filter panel beside a result list. topic=Computing is ticked. Topic is
labelled
The same panel with length=long ticked instead. The two labels have swapped.
Length holds 42, 22 and 14, and Topic falls to 9, 8, 6 and
5.
Both ticked. Each group is labelled

Several values at once

Put more than one value in terms and they widen the group. Add a second group and the two narrow together:

Selection Results
Computing 48
FLOSS 46
Computing and FLOSS, one group 59
Computing, plus long in Length 9

Values inside one group are an OR, because a reader ticking a second box wants more results. Separate groups are an AND, because each one is a different question about the same article.

Keep fq for your own rules

fq narrows everything, including the group the reader is working in. Once Computing pulls the other topics down to their overlap with it, the panel can no longer show the size of a switch to FLOSS.

Keep fq for the rules your product enforces, such as hiding paid content from a signed-out visitor. See Filter Syntax (fq).

Range facets

Group a numeric or date field into buckets you name:

{
  "facets": [
    {
      "field": "custom_attributes.word_count",
      "ranges": [
        {
          "to": 200,
          "key": "short"
        },
        {
          "from": 200,
          "to": 600,
          "key": "medium"
        },
        {
          "from": 600,
          "key": "long"
        }
      ]
    }
  ]
}
{
  "facet_counts": {
    "facet_fields": {
      "custom_attributes.word_count": [["medium", 1495], ["long", 538], ["short", 535]]
    }
  }
}

from is inclusive, to is exclusive, and each bucket needs at least one of them. The response uses your key, and still sorts by count.