POST /v2/search/{route} ranks a route's records by BM25 full text, by cosine similarity over vectors you supply, or by both merged — but full-text search only sees records you have explicitly indexed, and v2 has no embedding model.
POST /v2/search/{route} ranks the records on one route and returns their keys,
best match first. It has two independent arms — BM25 full-text over words you have indexed,
and cosine similarity over vectors you supply — and it can run both and merge the results.
Two things decide whether it will work for you, and both surprise people:
POST /v2/index/{route}/{key} for every record, and again
every time that record changes. A route you have never indexed returns
"results":[] with a 200, not an error.vector field, and pass a query vector on every search.Everything on this page was run against https://api2.infiniteocean.io and the
responses are pasted verbatim. Scores depend on the whole corpus, so the numbers you get on
your own route will differ from the numbers here — the shapes and the status codes will not.
Write a record, index it, search for it. All three steps are required. This is one uninterrupted run against an empty route.
# 1. write the record
curl -s -X POST https://api2.infiniteocean.io/v2/drop \
-H "Content-Type: application/json" \
-H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"route":"_docs_probe/quickstart","key":"alpha",
"payload":{"title":"Harbour pilot manual",
"body":"The pilot guides vessels through the harbour approach channel."}}'
# 200
# {"ok":true,"seq":1787577342969817,"offset":0,"len":177,"ts":1787594139842,
# "state":"accepted","rule_created":false,"drop_id":"1787577342969817",
# "route":"_docs_probe/quickstart","key":"alpha"}
# 2. search BEFORE indexing — the record is stored, but not findable
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/quickstart \
-H "Content-Type: application/json" \
-H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"q":"harbour"}'
# 200
# {"ok":true,"route":"_docs_probe/quickstart","q":"harbour","mode":"text","index_type":"bm25",
# "vector_status":"not_enabled","total_vectors":0,"dimensions":0,"results":[],"count":0}
# 3. index it
curl -s -X POST https://api2.infiniteocean.io/v2/index/_docs_probe/quickstart/alpha \
-H "X-Ocean-Key: $OCEAN_KEY"
# 200
# {"ok":true,"route":"_docs_probe/quickstart","key":"alpha",
# "terms_indexed":11,"fields_indexed":2,"links_indexed":0,"nums_indexed":0}
# 4. search again
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/quickstart \
-H "Content-Type: application/json" \
-H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"q":"harbour"}'
# 200
# {"ok":true,"route":"_docs_probe/quickstart","q":"harbour","mode":"text","index_type":"bm25",
# "vector_status":"not_enabled","total_vectors":0,"dimensions":0,
# "results":[{"key":"alpha","score":0.3956}],"count":1}
Step 2 is the single most common reason a search "does not work": a 200 with an empty result list means nothing on that route has been indexed, not that nothing matched.
The route is everything after /v2/search/ and may contain slashes:
/v2/search/_docs_probe/search searches the route _docs_probe/search.
Authentication. X-Ocean-Key is required on every search.
Authorization: Bearer is not accepted — sending the same key that way returns 401
KEY_REQUIRED. A key with read access is enough; a read-only app key searches
successfully.
Query string. This endpoint accepts no query parameters at all. Any
parameter — including plausible ones like ?top=5 — returns 400
UNKNOWN_PARAM and nothing is applied. Everything goes in the JSON body.
The body must be a JSON object and must contain q (or text) or
vector, or both. Any field not in this table returns 400
UNKNOWN_FIELD.
| Field | Type | Meaning |
|---|---|---|
q | string | Full-text query. Alias: text — the response echoes it back as q either way. An empty string counts as absent. A non-string returns 400. |
vector | array of numbers | Query vector for cosine similarity. Integers and floats both accepted. 1 to 4096 elements. |
top | number | Maximum results. Default 10. Aliases: top_k, limit — all three do the same thing on both arms. 0 means "use the default". Floats are truncated (1.9 behaves as 1). Negative values and strings return 400. |
min_score | number | Drop vector hits whose cosine score is below this. Default 0, which silently drops every negative-similarity record — see The default min_score hides opposites. Applies to the vector arm only; accepted and ignored on a text-only search. |
mode | string | "text" runs the text arm only even if you sent a vector. "vector" runs the vector arm only even if you sent q. Any other value, including "hybrid" and misspellings, is ignored and the arms are chosen from which fields you sent. A non-string returns 400. |
fusion | string | Accepted and ignored. See Fields that are accepted but do nothing. |
filter, where | any | Accepted and ignored. See Fields that are accepted but do nothing. |
| Field | Meaning |
|---|---|
results |
Array of {"key":…,"score":…}, highest score first. Keys only — no payloads. Fetch bodies with GET /v2/entity/{route}/{key}. |
count | Length of results. Not a corpus total. |
mode | Which arms actually ran: "text", "vector" or "hybrid". |
index_type | "bm25" when only the text arm ran, "kvec_stream" whenever the vector arm ran. |
vector_status | "ready" if the vector arm ran, "not_enabled" if it did not. It describes this request, not the route. |
total_vectors | How many records on the route carried a usable vector of the same length as your query. Counted before min_score is applied, so it can be larger than count. 0 on a text-only search. |
dimensions | Length of the query vector. 0 on a text-only search. |
q | Echoed back verbatim when you sent one. Absent on a pure vector search. |
There is no field reporting whether a vector scan ran to completion, and no field reporting the size of the indexed corpus.
Ranking is BM25 (k1 = 1.2, b = 0.75) over the terms you indexed. The examples in this
section run against _docs_probe/search, a route holding twelve short records,
eleven of them indexed.
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/search \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"q":"dredging channel","top":5}'
# 200
# {"ok":true,"route":"_docs_probe/search","q":"dredging channel","mode":"text",
# "index_type":"bm25","vector_status":"not_enabled","total_vectors":0,"dimensions":0,
# "results":[{"key":"gamma","score":2.7615},{"key":"alpha","score":1.0008}],"count":2}
Both words are searched independently and the scores are summed — this is an OR, not an
AND. gamma contains both "dredging" and "channel" and scores highest;
alpha contains only "channel" and still appears.
The whole record is treated as opaque text and tokenized. This has consequences worth knowing:
title
matches every record that has a title field. Verified: {"q":"title"}
returned the three records that have one.{"q":"HARBOUR"} and {"q":"harbour"} returned byte-identical
responses.{"q":"Øresund"},
{"q":"Ægir"} and {"q":"sejler"}, but not by
{"q":"øresund"} or {"q":"ægir"} — both return
"count":0. If your data is not English, normalise case yourself before writing
and before querying.{"q":"2019"} and {"q":"42"}
both matched the record containing them.{"q":"the"} and {"q":"a"} return
"count":0."count":0.{"q":"harbours"} does not match "harbour".The query string is tokenized the same way your documents are. Operators are not parsed — they are stripped as punctuation, or treated as ordinary characters. All of these were run against records containing "harbour" and "pilot":
{"q":"\"harbour pilot\""} — quotes are discarded. This is not a phrase
search; it returned a byte-identical response to the plain harbour pilot OR
query.{"q":"harbour -pilot"} — the minus is discarded. Also byte-identical to
the plain query; nothing was excluded.{"q":"harb*"} and {"q":"harb"} — no wildcards, no prefix
matching. Both returned "count":0 against records containing "harbour".Scores are raw BM25 sums. They are not normalised to 0–1, they are comparable only within one response, and they can be negative. Do not treat a negative score as an error and do not compare scores across two searches.
Negative scores are a signal about your indexing, not about the query. They appear when the corpus statistics the ranking uses have fallen behind the postings, and the way to get there is to index concurrently. Both of these were run in the same session, minutes apart:
# A: ten records, each containing "mooring", indexed with 10 parallel requests
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/negidf \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"q":"mooring","top":20}'
# 200
# {"ok":true,...,"results":[{"key":"d10","score":-0.8921},{"key":"d1","score":-0.9651},
# {"key":"d2","score":-0.9651},{"key":"d3","score":-0.9651},{"key":"d4","score":-0.9651},
# {"key":"d5","score":-0.9651},{"key":"d6","score":-0.9651},{"key":"d7","score":-0.9651},
# {"key":"d8","score":-0.9651},{"key":"d9","score":-0.9651}],"count":10}
# B: eight equivalent records, indexed ONE AT A TIME
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/seqidf \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"q":"mooring","top":20}'
# 200
# {"ok":true,...,"results":[{"key":"s1","score":0.0572},{"key":"s2","score":0.0572},
# {"key":"s3","score":0.0572},{"key":"s4","score":0.0572},{"key":"s5","score":0.0572},
# {"key":"s6","score":0.0572},{"key":"s7","score":0.0572},{"key":"s8","score":0.0572}],
# "count":8}
Index one record at a time. Ordering still works when the statistics are skewed — every record in run A ranks in a sane order — but the magnitudes are wrong, and a client that thresholds on score will reject everything.
Scores also differ between nodes for the same query, because each node keeps its own copy
of those statistics. The same record and the same query returned 2.2683 from one
node and 1.7845 from another, seconds apart. Rank by the order in the response;
do not persist a score and compare it later.
The default top is 10. A query that matched eleven records returned ten with
top omitted and all eleven with "top":20.
A text search does one index lookup per token in q, sequentially. Latency
scales with how many words you send, not with how big the route is. Measured from one client
over the public internet against an eleven-record route, on consecutive requests:
Words in q | Run 1 | Run 2 |
|---|---|---|
| 1 | 2.84 s | 3.08 s |
| 2 | 3.88 s | 3.77 s |
| 10 | 11.14 s | 11.06 s |
| 40 | 44.60 s | — |
That is roughly a fixed couple of seconds plus about a second per word. Practical consequences:
{"q":"harbour harbour harbour"} returned
exactly three times the score of {"q":"harbour"} for every hit
(3.2672 / 2.9720 / 2.2137 against 1.0891 / 0.9907 / 0.7379).You bring the vectors. v2 has no embedding endpoint and no model — it will not convert
q into a vector for you. The workflow is:
vector field.vector.No indexing call is needed for vector search. The vector arm streams the route's current records and scores them directly, so a record is searchable as soon as it is readable.
# store records that carry vectors
curl -s -X POST https://api2.infiniteocean.io/v2/drop \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"route":"_docs_probe/vecs","key":"alpha",
"payload":{"label":"east","vector":[1.0,0.0,0.0,0.0]}}'
# search by cosine similarity
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/vecs \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"vector":[1.0,0.0,0.0,0.0],"top_k":3}'
# 200
# {"ok":true,"route":"_docs_probe/vecs","mode":"vector","index_type":"kvec_stream",
# "vector_status":"ready","total_vectors":3,"dimensions":4,
# "results":[{"key":"alpha","score":1.0000},{"key":"gamma","score":0.9939},
# {"key":"beta","score":0.0000}],"count":3}
Scores are cosine similarity. Orthogonal vectors score 0.0000 and are still returned. Use
min_score to cut them:
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/vecs \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"vector":[1.0,0.0,0.0,0.0],"top_k":10,"min_score":0.5}'
# 200
# {"ok":true,"route":"_docs_probe/vecs","mode":"vector","index_type":"kvec_stream",
# "vector_status":"ready","total_vectors":3,"dimensions":4,
# "results":[{"key":"alpha","score":1.0000},{"key":"gamma","score":0.9939}],"count":2}
The default is min_score: 0, and the comparison is strict — a record whose
cosine similarity is negative is dropped from results but still counted in
total_vectors. That is how a search comes back with
"total_vectors":3,"count":2 and no other explanation. Pass
"min_score":-1 to see everything:
# route holds three 4-element vectors; one points the opposite way
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/vecneg \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"vector":[1.0,0.0,0.0,0.0]}'
# 200
# {"ok":true,...,"total_vectors":3,"dimensions":4,
# "results":[{"key":"nested","score":1.0000},{"key":"same","score":1.0000}],"count":2}
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/vecneg \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"vector":[1.0,0.0,0.0,0.0],"min_score":-1}'
# 200
# {"ok":true,...,"total_vectors":3,"dimensions":4,
# "results":[{"key":"nested","score":1.0000},{"key":"same","score":1.0000},
# {"key":"opposite","score":-1.0000}],"count":3}
Two shapes are read: a top-level vector field, and a
vector inside a top-level payload object. Nothing else is. Three
records were written to one route — one with {"embedding":[…]}, one with
{"meta":{"vector":[…]}}, one with {"vector":[…]} — and a search
reported "total_vectors":1, matching only the third:
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/vecshape \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"vector":[1.0,0.0,0.0,0.0]}'
# 200
# {"ok":true,"route":"_docs_probe/vecshape","mode":"vector","index_type":"kvec_stream",
# "vector_status":"ready","total_vectors":1,"dimensions":4,
# "results":[{"key":"c-toplevel","score":1.0000}],"count":1}
The nested form works too — the record keyed nested in the
min_score example above was written as
{"payload":{"vector":[1.0,0.0,0.0,0.0]}} and scored 1.0000. Prefer the top-level
form; the nested one exists because a payload can itself contain a payload key.
Records with no vector are skipped silently — they cost a scan but never appear in results
and are not counted in total_vectors.
A record whose vector is a different length than the query vector is skipped, exactly like
a record with no vector at all. If that leaves nothing, the response is 404
NO_VECTORS — which means "nothing on this route matched your query
vector's shape", not "this route is empty". A 4-element query against a route of 4-element
vectors works; a 3-element query against the same route returns:
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/vecs \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"vector":[1.0,0.0,0.0]}'
# 404
# {"ok":false,"error":"no vectors at this location","code":"NO_VECTORS"}
So a NO_VECTORS in production usually means you changed embedding model and the
new query dimension no longer matches what is stored. Re-embed and rewrite the records.
Vectors may have 1 to 4096 elements. A 4096-element query is accepted (it reached the scan
and returned NO_VECTORS against 4-element records); 4097 returns 400
INVALID_VECTOR, as does an empty array, a non-array, and an array containing
anything that is not a number.
Send q and vector in the same request and both arms run. Each arm's
scores are normalised by that arm's own maximum, halved, and added, so a record found by both
arms outranks one found by a single arm. A record that tops one arm and is absent from the other
scores exactly 0.5000; a record that tops both scores exactly 1.0000.
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/hybrid \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"q":"harbour","vector":[0.0,1.0,0.0,0.0],"top":5}'
# 200
# {"ok":true,"route":"_docs_probe/hybrid","q":"harbour","mode":"hybrid",
# "index_type":"kvec_stream","vector_status":"ready","total_vectors":3,"dimensions":4,
# "results":[{"key":"gamma","score":0.5552},{"key":"beta","score":0.5000},
# {"key":"alpha","score":0.5000}],"count":3}
Hybrid needs the route indexed for the text half and carrying vectors for the vector half. If
one arm finds nothing you still get the other arm's hits, at half weight, and the response still
says "mode":"hybrid":
# text arm matches nothing — vector hits come back at half weight
-d '{"q":"zzzznomatch","vector":[0.0,1.0,0.0,0.0]}'
# 200
# {"ok":true,...,"mode":"hybrid","total_vectors":3,"dimensions":4,
# "results":[{"key":"beta","score":0.5000},{"key":"gamma","score":0.0552},
# {"key":"alpha","score":0.0000}],"count":3}
A hybrid search silently degrades to text-only if your query vector's dimensions do
not match what is stored. The 404 NO_VECTORS described above fires only for
vector-only searches. In hybrid you get a 200, text results, and the only signal is
"total_vectors":0:
# 3-element query vector against a route of 4-element vectors
-d '{"q":"harbour","vector":[1.0,0.0,0.0]}'
# 200
# {"ok":true,...,"mode":"hybrid","index_type":"kvec_stream","vector_status":"ready",
# "total_vectors":0,"dimensions":3,
# "results":[{"key":"alpha","score":0.5000},{"key":"gamma","score":0.5000}],"count":2}
If you rely on hybrid, assert on total_vectors > 0 in your client. Nothing else
in the response tells you the vector half did no work — vector_status still says
"ready" and index_type still says "kvec_stream".
min_score filters the vector arm before the merge; it never removes a text-only
hit.
Use mode to run one arm from a request that carries both fields, which is handy
when you want to compare them without building two request bodies:
# same body, "mode":"text" — the vector is ignored
-d '{"q":"harbour","vector":[0.0,1.0,0.0,0.0],"mode":"text"}'
# {"ok":true,...,"mode":"text","index_type":"bm25","vector_status":"not_enabled",
# "total_vectors":0,"dimensions":0,
# "results":[{"key":"alpha","score":0.4700},{"key":"gamma","score":0.4700}],"count":2}
# same body, "mode":"vector" — q is ignored
-d '{"q":"harbour","vector":[0.0,1.0,0.0,0.0],"mode":"vector"}'
# {"ok":true,...,"mode":"vector","index_type":"kvec_stream","vector_status":"ready",
# "total_vectors":3,"dimensions":4,
# "results":[{"key":"beta","score":1.0000},{"key":"gamma","score":0.1104},
# {"key":"alpha","score":0.0000}],"count":3}
This is the call that makes a record findable by q. It requires
write access to the route: an app key scoped to the route with
can_write:false searches it successfully and gets 403
APPKEY_OUT_OF_SCOPE from this endpoint. The last path segment is the key;
everything between /v2/index/ and that segment is the route.
curl -s -X POST https://api2.infiniteocean.io/v2/index/_docs_probe/quickstart/beta \
-H "X-Ocean-Key: $OCEAN_KEY"
# 200
# {"ok":true,"route":"_docs_probe/quickstart","key":"beta","terms_indexed":9,
# "fields_indexed":2,"links_indexed":0,"nums_indexed":0}
terms_indexed counts the postings written: the record's distinct searchable
terms plus one internal length record, so it is always one higher than the number of distinct
words you can search for. (A record whose text reduced to exactly four searchable words
reported "terms_indexed":5.) The other three counters belong to sibling
features — field lookups, linked records and numeric summaries — that the same call maintains
alongside the text index.
Overwriting a record leaves the old terms in place until you re-index. Verified end to end
on one route: beta was written with the word "drift", indexed, and found by
{"q":"drift"}. It was then rewritten so its text said "harbour" instead:
# after the rewrite, BEFORE re-indexing
-d '{"q":"harbour"}' # {"results":[{"key":"alpha","score":0.9163}],"count":1} — beta missing
-d '{"q":"drift"}' # {"results":[{"key":"beta","score":0.9930}],"count":1} — stale term still hits
# POST /v2/index/_docs_probe/quickstart/beta -> 200
# after re-indexing
-d '{"q":"harbour"}' # {"results":[{"key":"beta","score":0.2612},
# {"key":"alpha","score":0.2410}],"count":2}
-d '{"q":"drift"}' # {"results":[],"count":0}
So re-indexing both adds the new terms and removes the ones that left the record.
Note also that alpha's score moved from 0.9163 to 0.2410 without
alpha changing at all — BM25 is relative to the corpus, so indexing anything
reshuffles everything.
There is no bulk indexing endpoint. One call per record.
If you send a JSON body, the index is built from that body instead of from the stored record. The stored record is not modified. This lets you add synthetic search text — tags, a summary, an alternate spelling — without polluting the data your application reads back:
curl -s -X POST https://api2.infiniteocean.io/v2/index/_docs_probe/quickstart/gamma \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"title":"Harbour dredging report",
"body":"Dredging of the harbour basin deepened the channel to twelve metres.",
"extra":"bathymetry survey"}'
# 200
# {"ok":true,"route":"_docs_probe/quickstart","key":"gamma","terms_indexed":14,
# "fields_indexed":3,"links_indexed":0,"nums_indexed":0}
curl -s -X POST https://api2.infiniteocean.io/v2/search/_docs_probe/quickstart \
-H "Content-Type: application/json" -H "X-Ocean-Key: $OCEAN_KEY" \
-d '{"q":"bathymetry"}'
# 200
# {"ok":true,...,"results":[{"key":"gamma","score":0.8898}],"count":1}
curl -s https://api2.infiniteocean.io/v2/entity/_docs_probe/quickstart/gamma \
-H "X-Ocean-Key: $OCEAN_KEY"
# 200
# {"title":"Harbour dredging report",
# "body":"Dredging of the harbour basin deepened the channel to twelve metres."}
The body form replaces the previous term set for that key, same as the no-body form. If you use it, use it every time for that record, or the synthetic terms will disappear on the next plain re-index.
Requests are load-balanced across nodes in several regions. A write goes to one node and the index call that follows may land on another, before the record has propagated. Because the no-body form reads the record before indexing it, it returns 404. This is a race, not a certainty — writing on one node and indexing on another with no delay reproduced it once in three attempts:
# {"ok":false,"error":"entity not found — nothing was indexed","code":"NOT_FOUND"} 404
Two fixes, both verified:
The same 404 is what you get for a key that genuinely does not exist, so you cannot tell the two apart from the response.
The text index is itself stored as data, so it propagates the same way. A record written and
indexed on one node was searched on another immediately afterwards: the first search returned
"count":0, the next one — a few seconds later — returned the record. Do not assert
on a search result in the same breath as the write that should have produced it.
Three body fields pass validation and have no effect. They return 200 with unfiltered results, so a filter you believe is applied is not:
| Field | What actually happens |
|---|---|
filter | Ignored. {"q":"harbour","filter":{"title":"nope"}} returned a byte-identical response to {"q":"harbour"} alone. |
where | Ignored. Same byte-identical result. |
fusion | Ignored. "fusion":"rrf" is accepted; hybrid merging is always the weighted scheme described above. |
To restrict results by field value, do not use filter. Use
POST /v2/query for structured filtering, or filter the returned keys yourself after
fetching them. An unrecognised mode string behaves the same way — both
"mode":"hybrid" and "mode":"banana" returned the plain text-only
response, silently ignored rather than rejected.
| Status | Code | Cause |
|---|---|---|
| 401 | KEY_REQUIRED |
No X-Ocean-Key header. Also returned when you send Authorization: Bearer instead. Applies to both /v2/search/… and /v2/index/…. |
| 403 | FORBIDDEN |
Your account key cannot reach the route, or the route does not exist. The two are not distinguished. |
| 403 | APPKEY_OUT_OF_SCOPE |
An app key was used outside its scope: a route it does not cover, or POST /v2/index/… with can_write:false. |
| 400 | SEARCH_REQUIRED |
Body is empty, or contains neither q/text nor vector. An empty-string q with no vector counts as missing. |
| 400 | UNKNOWN_FIELD |
A body field that is not in the request table — e.g. fuzzy, cursor, offset. |
| 400 | INVALID_JSON |
Body is not parseable JSON, or is valid JSON but not an object (an array returns this too). |
| 400 | INVALID_INPUT |
Two different messages share this code. "invalid search query" means q is longer than 512 bytes. "invalid search body" means a field had the wrong type or value — q not a string, top negative or a string, min_score a string, mode or fusion not a string. |
| 400 | INVALID_VECTOR |
vector is not an array, is empty, has more than 4096 elements, or contains a non-number. |
| 400 | UNKNOWN_PARAM |
Any URL query parameter. This endpoint takes none. |
| 404 | NO_VECTORS |
Vector-only search where no record on the route had a vector of the same length as the query. |
| 404 | NOT_FOUND |
Wrong method or malformed path — GET /v2/search/… and POST /v2/search/ with no route both return "no /v2 endpoint matches this method and path". From /v2/index/…, it means the record was not found on the node that served the request. |
Every 4xx from this endpoint is a hard rejection with nothing applied — there is no partial search. A zero-result search is a 200, never a 404.
| Limit | Value | Behaviour when exceeded |
|---|---|---|
| Query length | 512 bytes | 400 INVALID_INPUT — confirmed at the boundary, 512 accepted and 513 rejected. Note a 512-byte query may take minutes; see Cost. |
| Vector dimensions | 1–4096 | 400 INVALID_VECTOR — confirmed at the boundary, 4096 accepted and 4097 rejected. |
| Term length | 128 bytes | Longer tokens are neither indexed nor matchable — confirmed with a 200-character word. |
| Results per text search | 100 | Not measured. "top":500 is accepted and returns 200. |
| Results per vector search | 1000 | Not measured. "top_k":5000 is accepted and returns 200. |
| Records scanned per vector search | 100,000 | Not measured, and no response field reports whether the scan was cut short. |
| Terms indexed per record | 2,000 | Not measured. |
The first three rows were confirmed at the boundary against the live service. The last four are the service's stated ceilings; the probe corpus was far too small to reach them, so treat those numbers as documentation rather than as measurements.
There is no cursor or offset on search — cursor and offset in the
body both return 400 UNKNOWN_FIELD. To page, raise top and slice
client-side.
Things developers look for and will not find. None of these is behind a flag or a plan tier — there is no endpoint:
q and get
semantic matching, you will get BM25 keyword matching instead./v2/search/{route}
searches exactly one route. There is no "search everything" door.filter and where are
accepted and ignored; use POST /v2/query for predicates.https://api2.infiniteocean.io · authenticate with the X-Ocean-Key header.