## Search comments **post** `/v2/comments/search` Search comments with a structured filter AST, full-text search, sorting, and cursor pagination. ### Two modes (combinable) - **Filter mode** — pass a structured `query` AST (and/or `sort`) and the endpoint returns comments matching the filter, ordered by `sort` (default `createdAt:desc`). Same shape as `/v2/posts/search`, `/v2/conversations/search`, `/v2/companies/search`, `/v2/contacts/search`. - **Search mode** — pass a top-level `search` string and the endpoint runs a hybrid full-text + vector search over comment `content`, returning matches ranked by relevance. Optional: combine with `query` to constrain the search to a post / changelog / author / time window, and/or `sort` to override relevance ranking with chronological order. ```json { "search": "dark mode feedback", "query": { "operator": "AND", "value": [ { "field": "postId", "operator": "=", "value": "507f1f77bcf86cd799439011" } ] }, "limit": 20 } ``` `search` is server-managed: no operators, no field selection, no special syntax — same plain-text contract as `/v2/posts/search`. The string must be 1–500 characters. ### Structured query AST Each clause has the shape `{ field, operator, value }`. You can pass a single clause or wrap up to 15 in a top-level `AND` group. To list every comment a contact has authored: ```json { "query": { "field": "authorId", "operator": "=", "value": "507f1f77bcf86cd799439011" }, "sort": "createdAt:desc", "limit": 100 } ``` Top-level `OR` groups and nested groups are not supported in this version. ### Supported fields and operators Field names are camelCase, matching the rest of the v2 public API. The other v2 search endpoints (posts, contacts, companies, conversations) shipped earlier with snake_case field names and continue to accept those for back-compat; comments search is greenfield and ships with camelCase only. | Field | Type | Operators | | ------------------------------------- | ----------------------- | --------------------------------------------------------------------------- | | `authorId` | string (user id) | `=`, `!=`, `IN`, `NIN` | | `postId` | id or null (submission) | `=`, `!=`, `IN`, `NIN` (use `null` for changelog-only comments) | | `changelogId` | id or null | `=`, `!=`, `IN`, `NIN` (use `null` for post-only comments) | | `parentCommentId` | id or null | `=`, `!=`, `IN`, `NIN` (use `null` for root comments, non-null for replies) | | `isPrivate` | boolean | `=`, `!=` | | `inReview` | boolean | `=`, `!=` | | `createdAt` | unix seconds | `=`, `!=`, `>`, `<`, `>=`, `<=` | | `upvotes`, `score`, `confidenceScore` | number | `=`, `!=`, `>`, `<`, `>=`, `<=` | Use the top-level `search` field for full-text search across comment `content` (see "Search mode" above). The clause-level operators `~`, `!~`, `^`, `$` are reserved for future use and currently return `400`. A filter-mode query (no `search`) consisting only of `!=` or `NIN` clauses on unbounded fields is rejected with `query_too_broad` to prevent full-org scans. Combine the negation with at least one positive clause (`=`, `IN`, `>`, `<`) instead. Boolean fields (`isPrivate`, `inReview`) are bounded so a standalone negation on them is allowed. Search mode (with `search` set) relaxes this — the FTS query itself acts as the positive constraint. ### Sort Allowed values: - `createdAt:desc` (default in filter mode), `createdAt:asc` — newest / oldest first - `confidenceScore:desc` (the dashboard's "Best" view), `confidenceScore:asc` (low-confidence first, useful for moderation) - `score:desc` — raw upvotes − downvotes ("Top") Numeric counters (`upvotes`, `score`, `confidenceScore`) remain available as filter fields with the full range of operators; only the sort axes above are surfaced to keep the cursor-pagination contract small. `score:asc` and `upvotes` (both directions) are deliberately not exposed: `upvotes:desc` is identical to `score:desc` for the vast majority of comments (no downvotes), and "most-downvoted-first" / "least-upvoted-first" have no product use case. When `search` is set, results are ranked by relevance unless `sort` is also set, in which case the matching comments are returned in the requested order. The chosen sort axis (or relevance) is encoded in the cursor; switching `sort` (or toggling `search` on/off) mid-pagination returns `400 invalid_cursor`. Restart pagination without a cursor when changing the sort or search shape. ### Pagination Cursor-based, `limit` between 1 and 100 (default 10). In filter mode, `totalCount` is approximate and capped at 5000; `totalCountCapped` is `true` when the real count may be higher. In search mode, `totalCount` reflects the matching survivor set after the AST filter and is capped at 200 (the Turbopuffer top-K). ### Server-side guards These visibility rules are always applied and CANNOT be widened via the AST: - Callers without the `view_comments_private` permission can only see public comments. Sending `{ field: "isPrivate", operator: "=", value: true }` as such a caller returns an empty result, NOT a 403 — the security floor wins. - Callers without the `moderate_comments` permission can only see in-review comments they authored themselves. `inReview` queries from such callers are intersected with the authored-by-self constraint. - Spam comments (`isSpam: true`) are NOT excluded — this matches the legacy `GET /v2/comments` behavior. If you want to exclude spam, filter on the `isSpam` field (NOT exposed today; track follow-up). ### Response Returns a standard list envelope with comment rows identical to `GET /v2/comments/{id}`. ### Version Availability This endpoint is only available in API version 2026-01-01.nova and newer. ### Header Parameters - `"Featurebase-Version": optional "2026-01-01.nova" or "2025-12-12.clover"` - `"2026-01-01.nova"` - `"2025-12-12.clover"` ### Body Parameters - `cursor: optional string` An opaque cursor for pagination. Use the `nextCursor` value from a previous response to fetch the next page of results. - `limit: optional number` A limit on the number of objects to be returned, between 1 and 100. - `query: optional object { field, operator, value } or object { operator, value }` Structured filter AST. Either a single filter clause or one top-level AND group (max 15 clauses). Top-level OR groups are not yet supported. - `SearchFilter object { field, operator, value }` - `field: string` Field name to filter on (e.g. `state`, `tag_ids`, `created_at`) - `operator: "=" or "!=" or "IN" or 9 more` Comparison operator - `"="` - `"!="` - `"IN"` - `"NIN"` - `">"` - `"<"` - `">="` - `"<="` - `"~"` - `"!~"` - `"^"` - `"$"` - `value: string or number or boolean or array of string or number` Value to compare against (primitive or array of primitives) - `string` - `number` - `boolean` - `array of string or number` - `string` - `number` - `SearchFilterGroup object { operator, value }` - `operator: "AND" or "OR"` Group operator: AND (all match) or OR (any match) - `"AND"` - `"OR"` - `value: array of object { field, operator, value }` Array of filter clauses (1-15 entries) - `field: string` Field name to filter on (e.g. `state`, `tag_ids`, `created_at`) - `operator: "=" or "!=" or "IN" or 9 more` Comparison operator - `"="` - `"!="` - `"IN"` - `"NIN"` - `">"` - `"<"` - `">="` - `"<="` - `"~"` - `"!~"` - `"^"` - `"$"` - `value: string or number or boolean or array of string or number` Value to compare against (primitive or array of primitives) - `string` - `number` - `boolean` - `array of string or number` - `string` - `number` - `search: optional string` Plain-text full-text search across comment `content`. Server-managed (hybrid BM25 + vector, semanticRatio 0.5) — no operators, no field selection. When `search` is set, results are ranked by relevance unless `sort` is also set, in which case the matching comments are returned in the requested order. Combine with `query` to constrain the search to a post / changelog / author / time window. - `sort: optional "createdAt:desc" or "createdAt:asc" or "confidenceScore:desc" or 2 more` Sort field + direction. Defaults to `createdAt:desc` in filter mode and to relevance in search mode. The chosen sort axis is encoded in the cursor; switching `sort` (or toggling `search` on/off) mid-pagination returns `400 invalid_cursor`. - `"createdAt:desc"` - `"createdAt:asc"` - `"confidenceScore:desc"` - `"confidenceScore:asc"` - `"score:desc"` ### Returns - `data: array of object { id, author, changelogId, 14 more }` Array of search results - `id: string` Unique identifier - `author: object { id, name, profilePicture, type }` - `id: string` Author unique identifier - `name: string` Author display name - `profilePicture: string` Author profile picture URL - `type: "admin" or "customer" or "guest" or 3 more` Type of user who authored the comment - `"admin"` - `"customer"` - `"guest"` - `"integration"` - `"bot"` - `"lead"` - `changelogId: string` Changelog ID this comment belongs to - `content: string` Comment content in HTML format - `createdAt: string` ISO 8601 timestamp when created - `downvotes: number` Number of downvotes - `inReview: boolean` Whether the comment is in review - `isDeleted: boolean` Whether the comment is deleted - `isPinned: boolean` Whether the comment is pinned - `isPrivate: boolean` Whether the comment is private - `isSpam: boolean` Whether the comment is spam - `object: "comment"` Object type identifier - `"comment"` - `parentCommentId: string` Parent comment ID for replies, null for root comments - `postId: string` Post ID this comment belongs to - `score: number` Net score (upvotes - downvotes) - `updatedAt: string` ISO 8601 timestamp when updated - `upvotes: number` Number of upvotes - `nextCursor: string` Cursor for fetching the next page (null if no more results) - `object: "list"` Object type identifier - `"list"` - `totalCount: optional number` Total number of comments matching the query, capped at 5000 in filter mode and 200 (the Turbopuffer top-K) in search mode. When at the cap, `totalCountCapped` is true and the value is exactly the cap. - `totalCountCapped: optional boolean` True when `totalCount` is exactly the cap and the real count may be higher. UI can render as e.g. "5000+". ### Example ```http curl https://do.featurebase.app/v2/comments/search \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $FEATUREBASE_API_KEY" \ -d '{ "cursor": "eyJpZCI6IjUwN2YxZjc3YmNmODZjZDc5OTQzOTAxMSJ9", "limit": 10, "search": "dark mode feedback", "sort": "createdAt:desc" }' ``` #### Response ```json { "data": [ { "id": "507f1f77bcf86cd799439011", "author": { "id": "507f1f77bcf86cd799439011", "name": "John Doe", "profilePicture": "https://cdn.example.com/avatars/john.png", "type": "customer" }, "changelogId": "507f1f77bcf86cd799439013", "content": "

This is a great idea!

", "createdAt": "2023-12-12T00:00:00.000Z", "downvotes": 0, "inReview": false, "isDeleted": false, "isPinned": false, "isPrivate": false, "isSpam": false, "object": "comment", "parentCommentId": "507f1f77bcf86cd799439014", "postId": "507f1f77bcf86cd799439012", "score": 5, "updatedAt": "2023-12-13T00:00:00.000Z", "upvotes": 5 } ], "nextCursor": "eyJpZCI6IjUwN2YxZjc3YmNmODZjZDc5OTQzOTAxMSJ9", "object": "list", "totalCount": 42, "totalCountCapped": false } ```