InfiniteOceanv2

Search

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.

What search does

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:

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.

Quick start

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.

POST /v2/search/{route}

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.

Request 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.

FieldTypeMeaning
qstring 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.
vectorarray of numbers Query vector for cosine similarity. Integers and floats both accepted. 1 to 4096 elements.
topnumber 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_scorenumber 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.
modestring "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.
fusionstring Accepted and ignored. See Fields that are accepted but do nothing.
filter, whereany Accepted and ignored. See Fields that are accepted but do nothing.

Response body

FieldMeaning
results Array of {"key":…,"score":…}, highest score first. Keys only — no payloads. Fetch bodies with GET /v2/entity/{route}/{key}.
countLength of results. Not a corpus total.
modeWhich 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_vectorsHow 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.
dimensionsLength of the query vector. 0 on a text-only search.
qEchoed 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.

Full-text search

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.

How your text is turned into terms

The whole record is treated as opaque text and tokenized. This has consequences worth knowing:

Query syntax that does not exist

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":

Reading the scores

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.

Cost: one round trip per word in your query

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 qRun 1Run 2
12.84 s3.08 s
23.88 s3.77 s
1011.14 s11.06 s
4044.60 s—

That is roughly a fixed couple of seconds plus about a second per word. Practical consequences:

Vector search

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:

  1. Embed your content with a model of your choice.
  2. Store the result in the record under a vector field.
  3. Embed the query with the same model and send that array as 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 min_score hides opposites

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}

Where the vector has to live

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.

Dimensions must match exactly

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.

Hybrid search

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}

POST /v2/index/{route}/{key} — keeping the text index current

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.

The index does not follow your writes

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.

Indexing text that is not in the 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.

Indexing immediately after a write can return 404

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.

Search results are eventually consistent across nodes

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.

Fields that are accepted but do nothing

Three body fields pass validation and have no effect. They return 200 with unfiltered results, so a filter you believe is applied is not:

FieldWhat actually happens
filterIgnored. {"q":"harbour","filter":{"title":"nope"}} returned a byte-identical response to {"q":"harbour"} alone.
whereIgnored. Same byte-identical result.
fusionIgnored. "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.

Errors

StatusCodeCause
401KEY_REQUIRED No X-Ocean-Key header. Also returned when you send Authorization: Bearer instead. Applies to both /v2/search/… and /v2/index/….
403FORBIDDEN Your account key cannot reach the route, or the route does not exist. The two are not distinguished.
403APPKEY_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.
400SEARCH_REQUIRED Body is empty, or contains neither q/text nor vector. An empty-string q with no vector counts as missing.
400UNKNOWN_FIELD A body field that is not in the request table — e.g. fuzzy, cursor, offset.
400INVALID_JSON Body is not parseable JSON, or is valid JSON but not an object (an array returns this too).
400INVALID_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.
400INVALID_VECTOR vector is not an array, is empty, has more than 4096 elements, or contains a non-number.
400UNKNOWN_PARAM Any URL query parameter. This endpoint takes none.
404NO_VECTORS Vector-only search where no record on the route had a vector of the same length as the query.
404NOT_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.

Limits

LimitValueBehaviour when exceeded
Query length512 bytes400 INVALID_INPUT — confirmed at the boundary, 512 accepted and 513 rejected. Note a 512-byte query may take minutes; see Cost.
Vector dimensions1–4096400 INVALID_VECTOR — confirmed at the boundary, 4096 accepted and 4097 rejected.
Term length128 bytesLonger tokens are neither indexed nor matchable — confirmed with a 200-character word.
Results per text search100Not measured. "top":500 is accepted and returns 200.
Results per vector search1000Not measured. "top_k":5000 is accepted and returns 200.
Records scanned per vector search100,000Not measured, and no response field reports whether the scan was cut short.
Terms indexed per record2,000Not 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.

What is not here

Things developers look for and will not find. None of these is behind a flag or a plan tier — there is no endpoint:

Base URL https://api2.infiniteocean.io · authenticate with the X-Ocean-Key header.