Query
Searches one or more collections for matching documents.#
/lmkit/v1/search/searchReturns the documents that best match the query, most relevant first, across a single collection or a set of collections. Pass 'collection_ids' to span several collections in one query, or 'collection_id' to search one. Use 'search_type' to choose how matching works: full-text matches on the words a document contains, semantic matches on meaning, and hybrid combines both. Full-text search is always available; semantic and hybrid require every searched collection to have semantic search enabled, and when several collections are searched together they must share the same embedding model, otherwise the request returns 400 Bad Request. Pass 'filters' to return only documents whose metadata contains the given key/value pairs, or 'filter' for the full operator algebra (eq, ne, gt, gte, lt, lte, in, nin, exists with and/or composition over typed values); the two are mutually exclusive. Use 'offset' with 'top_k' to page through results, 'sort' to order by relevance (default) or recency (full-text only), and 'facets' to get document counts per metadata value over the whole match set (full-text only). Retrieval shaping: 'max_chunks_per_document' returns up to N matched pages per document instead of one, 'recency_bias' folds an exponential freshness decay into every search type's relevance score, 'context_expansion' attaches the matched chunk's neighborhood or the full page to each hit for RAG prompt assembly, 'mmr' re-selects semantic results for diversity, and 'rerank_alpha' blends the reranker's judgment with the first-stage retrieval score. Query understanding: 'query_mode' rewrites a follow-up into a standalone question ('Contextual', with 'chat_history'), expands the query into alternative phrasings fused by rank ('MultiQuery'), or embeds a generated hypothetical answer in place of the query ('Hyde', semantic/hybrid only); all run on the tenant's 'query_model' and degrade to the original query when it is unavailable, with the outcome reported in 'query_understanding'. Each result identifies the matching document, its collection, the page that matched, a highlighted snippet, and the document's details. Set 'include_page_layout' to true to also return each matched page's stored layout (the text blocks and their positions captured at index time); the layout is the largest part of a hit, so it is off by default and responses grow significantly when enabled.
Request body
application/json ·
| Property | Type | Description |
|---|---|---|
cluster_id | string | The search cluster that holds the tenant and collection. If not provided, the default cluster is used. |
tenant_idrequired | string (uuid) | The tenant that owns the collection. |
collection_id | string (uuid) | A single collection to search. Ignored when 'collection_ids' is provided. |
collection_ids | string (uuid)[] | The set of collections to search in one query. Takes precedence over 'collection_id'. For semantic or hybrid search, every collection must share the same embedding model. |
queryrequired | string | The search query text. |
search_type | How to match: 'FullText' matches on the words a document contains (always available), 'Semantic' matches on meaning, and 'Hybrid' combines both. 'Semantic' and 'Hybrid' require the collection to have semantic search enabled. | |
top_k | object (int32) | Maximum number of documents to return. |
candidates | object (int32) | Hybrid search only. How many candidate matches to weigh before producing the final ranking; higher can improve quality, lower is faster. |
rerank | boolean | When true, a cross-encoder reranking pass re-scores the top retrieved candidates by direct query-document relevance and returns them in that order. It lifts precision in every mode (full-text, semantic, hybrid) at the cost of one extra inference pass over the candidates, so it is off by default and enabled per request when quality matters more than latency. |
rerank_top_n | object (int32) | How many top candidates the reranking pass re-scores when 'rerank' is true (clamped to top_k..200). This is the over-fetch: the store retrieves rerank_top_n candidates instead of top_k, the reranker re-scores them all, and the best top_k are returned, so a document the first-stage ranker placed below the top_k cut can still surface. A larger pool can find a better top result at the cost of more reranking work. Ignored when 'rerank' is false. Defaults to 50. |
rerank_alpha | object (double) | How much the reranker's judgment outweighs the first-stage retrieval score when 'rerank' is true, 0..1. At 1 (the default) the reranker's ordering and scores are used as-is. Below 1, both scores are min-max normalized over the candidate pool and blended as alpha*rerank + (1-alpha)*retrieval, letting strong retrieval evidence temper an overconfident cross-encoder. Defaults to 0.5: at 1 the cross-encoder replaces the retrieval ranking outright, which measured WORSE than not reranking at all on every corpus tried. Ignored when 'rerank' is false. |
max_chunks_per_document | object (int32) | Maximum result entries per document, 1..10. The default of 1 returns each matching document once, at its best-matching page. Raising it lets a strongly matching document surface up to that many of its matched pages as separate results (same document_id, different page_number), which suits RAG context assembly; 'top_k' still caps the total result count. |
recency_bias | object (double) | How much freshness matters, 0..1. Folds an exponential decay on each document's indexed-at timestamp into its relevance score (score * ((1-bias) + bias * 2^(-age/half_life))), for every search type. 0 (the default) changes nothing; at 1, a document one half-life old is worth half its original score, and a document with no indexed-at decays fully. Unlike sort=Recency (a hard newest-first ordering, full-text only), this blends freshness INTO relevance; the two cannot be combined. Applied over the retrieved candidate pool, before the top_k cut. |
recency_half_life_days | object (double) | The recency decay half-life in days (how old a document must be for its freshness weight to halve). Used only when 'recency_bias' is above 0. Defaults to 30. |
recency_key | string | Optional custom_metadata key whose ISO-8601 value dates each document for the recency decay. By default the decay reads indexed_at - but a bulk-imported archive is indexed today while its CONTENT is years old; naming the metadata key that carries the content date (e.g. 'date') makes freshness mean what the reader means. A document whose value is missing or unparseable falls back to indexed_at. Used only when 'recency_bias' is above 0. |
context_expansion | How much surrounding content each hit carries in its 'context' field. 'None' (default): no context. 'Chunk': the matched chunk plus its immediate neighbor chunks on the same page (semantic and hybrid matches; full-text matches are page-level and fall back to the full page). 'Page': the matched page's full stored Markdown. Context can be large; request it when the caller assembles RAG prompts from the results. | |
mmr | boolean | When true, re-selects the results with Maximal Marginal Relevance for diversity: each pick balances relevance against similarity to the already-picked results, so near-duplicate passages stop crowding out distinct ones. Semantic search only (400 otherwise); selection runs over the 'candidates' pool. Cannot be combined with 'rerank' or a non-zero 'offset'. |
query_mode | How the query is understood before retrieval. 'Original' (default): search with the query as sent. 'Contextual': rewrite a follow-up question into a self-contained query using 'chat_history'. 'MultiQuery': generate alternative phrasings ('query_variants' of them), retrieve for each, and fuse the ranked lists with Reciprocal Rank Fusion. 'Hyde': generate a hypothetical answer passage and embed it in place of the query for the semantic arm (semantic and hybrid only; opt-in, it can add latency and hurt precise queries). Every mode beyond 'Original' runs the tenant's query-understanding model ('query_model'), a small local LLM, adding one generation pass (typically tens to a few hundred milliseconds on a small model; MultiQuery additionally multiplies retrieval by the variant count). When no query model is configured or it cannot be loaded, the search degrades to the original query and 'query_understanding.applied_mode' in the response says so. | |
chat_history | [] | Prior conversation turns (oldest first) used by 'query_mode': for 'Contextual' it is required and drives the rewrite; for 'MultiQuery' and 'Hyde' it is optional and, when present, the query is contextualized first, mirroring grounded-chat behavior. |
query_variants | object (int32) | MultiQuery only: how many alternative phrasings to generate (1..5). The original query is always searched as well. Defaults to 3. |
mmr_lambda | object (double) | The MMR relevance/diversity balance, 0..1: 1 is pure relevance (plain top-k), 0 is pure diversity. Used only when 'mmr' is true. Defaults to 0.5. |
filters | object | Optional metadata filter: only documents whose custom_metadata contains all of these key/value pairs are returned. Applied during ranking, so top_k still returns the best matches that pass the filter. For operators beyond exact equality (ranges, or-composition, in/nin, exists), use 'filter' instead; the two are mutually exclusive. |
filter | one of object · | |
offset | object (int32) | Number of leading results to skip, for pagination. Use it with top_k as the page size (offset 0, then top_k, then 2*top_k, and so on); a page shorter than top_k is the last page. Defaults to 0. For hybrid search, paging depth is bounded by 'candidates'. |
sort | Result ordering. 'Relevance' (default) ranks by match score. 'Recency' orders most-recently-indexed first and is supported for full-text search only; semantic and hybrid searches return 400 Bad Request when 'Recency' is requested, because their results are top-K by relevance. | |
facets | string[] | Optional custom_metadata field names to compute facet counts for: document counts per value, not just over the returned page. Full-text counts the whole match set exactly; semantic and hybrid count the ranked candidate pool (deepened when facets are requested) and 'facets_exact' on the response says whether the counts cover everything. Best for low-cardinality fields such as department, status, or type. |
include_page_layout | boolean | When true, each result includes the matched page's stored layout (the text blocks and their positions captured at index time) in 'page_layout'. The layout is typically the largest part of a hit, so enabling it produces significantly larger responses; it is off by default, and the layout is then not read from storage at all. Enable it when the caller renders or highlights the matched page. |
min_score | object (double) | Optional relevance floor: drop results scoring below this value. For full-text the score is the text rank, for semantic it is cosine similarity (0..1), and for hybrid it is the normalized Reciprocal Rank Fusion score (0..1). Defaults to 0 (no floor). |
fusion | Hybrid search only. How the full-text and semantic arms are combined: 'Convex' (default: weighted combination of normalized scores, which preserves each arm's confidence) or 'Rrf' (Reciprocal Rank Fusion, rank-based). The arm weights apply to both. | |
full_text_weight | object (double) | Hybrid search only. Weight of the full-text arm in fusion. When omitted, the fusion mode's default applies: equal arms under Rrf, 1:9 favoring the semantic arm under Convex. |
semantic_weight | object (double) | Hybrid search only. Weight of the semantic arm in fusion. When omitted, the fusion mode's default applies: equal arms under Rrf, 1:9 favoring the semantic arm under Convex. |
name_boost | object (double) | Hybrid search only. Lifts documents whose name/title matches the query toward 1, by this fraction of the score's remaining headroom, so title hits rank above body-only hits while the fused score stays in 0..1. Range 0..1 (values outside are clamped); defaults to 0 (off). For example 0.2 closes 20% of the gap between a hit's fused score and 1. |
Responses
| Status | Type | Description |
|---|---|---|
| 200 | OK | |
| 400 | Bad Request | |
| 404 | Not Found | |
| 503 | application/json | Service Unavailable |
curl -X POST "$LMKIT_ONE_URL/lmkit/v1/search/search" \
-H "Authorization: Bearer $LMKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"cluster_id": "string",
"tenant_id": "string",
"collection_id": "string",
"collection_ids": [
"string"
],
"query": "",
"search_type": "FullText"
}'