From e6937a24d0b32cc7c52d21842526aab2e6a70a7c Mon Sep 17 00:00:00 2001 From: capJavert Date: Mon, 9 Mar 2026 12:30:08 +0100 Subject: [PATCH 1/4] feat(public-api): add experimental recommend endpoints for LLM consumption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new endpoints under /public/v1/recommend/ for surfacing daily.dev articles to AI agents and LLM-based tools: - /keyword — keyword-based search, supports pagination and time filters - /semantic — semantic search via Mimir, single-shot for LLM context Both marked experimental with x-daily-experimental header and response flag. Co-Authored-By: Claude Opus 4.6 --- __tests__/routes/public/recommend.ts | 152 ++++++++++++++++++ src/routes/public/index.ts | 2 + src/routes/public/recommend.ts | 227 +++++++++++++++++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 __tests__/routes/public/recommend.ts create mode 100644 src/routes/public/recommend.ts diff --git a/__tests__/routes/public/recommend.ts b/__tests__/routes/public/recommend.ts new file mode 100644 index 0000000000..d0704622eb --- /dev/null +++ b/__tests__/routes/public/recommend.ts @@ -0,0 +1,152 @@ +import request from 'supertest'; +import nock from 'nock'; +import { setupPublicApiTests, createTokenForUser } from './helpers'; + +const state = setupPublicApiTests(); + +const nockMimir = (postIds: string[]) => { + nock('http://localhost:7600') + .post('/v1/search') + .reply( + 204, + JSON.stringify({ + result: postIds.map((postId) => ({ postId })), + }), + ); +}; + +describe('GET /public/v1/recommend/keyword', () => { + it('should return keyword search results with experimental flag', async () => { + const token = await createTokenForUser(state.con, '5'); + nockMimir(['p1', 'p2']); + + const { body, headers } = await request(state.app.server) + .get('/public/v1/recommend/keyword') + .query({ q: 'javascript' }) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(body.experimental).toBe(true); + expect(body.method).toBe('keyword'); + expect(Array.isArray(body.data)).toBe(true); + expect(body.data.length).toBe(2); + expect(body.data[0]).toMatchObject({ + id: expect.any(String), + title: expect.any(String), + tags: expect.any(Array), + numUpvotes: expect.any(Number), + numComments: expect.any(Number), + }); + expect(body.pagination).toMatchObject({ + hasNextPage: expect.any(Boolean), + }); + expect(headers['x-daily-experimental']).toBeDefined(); + }); + + it('should support limit parameter', async () => { + const token = await createTokenForUser(state.con, '5'); + nockMimir(['p1']); + + const { body } = await request(state.app.server) + .get('/public/v1/recommend/keyword') + .query({ q: 'typescript', limit: 1 }) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(body.data.length).toBeLessThanOrEqual(1); + }); + + it('should support time filter', async () => { + const token = await createTokenForUser(state.con, '5'); + nockMimir(['p1']); + + const { body } = await request(state.app.server) + .get('/public/v1/recommend/keyword') + .query({ q: 'react', time: 'month' }) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(body.experimental).toBe(true); + expect(body.method).toBe('keyword'); + }); + + it('should return empty data when no results', async () => { + const token = await createTokenForUser(state.con, '5'); + nockMimir([]); + + const { body } = await request(state.app.server) + .get('/public/v1/recommend/keyword') + .query({ q: 'nonexistenttopic' }) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(body.data).toEqual([]); + }); + + it('should require authentication', async () => { + await request(state.app.server) + .get('/public/v1/recommend/keyword') + .query({ q: 'test' }) + .expect(401); + }); +}); + +describe('GET /public/v1/recommend/semantic', () => { + it('should return semantic search results with experimental flag', async () => { + const token = await createTokenForUser(state.con, '5'); + nockMimir(['p1', 'p2']); + + const { body, headers } = await request(state.app.server) + .get('/public/v1/recommend/semantic') + .query({ q: 'how do I make my chatbot remember things' }) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(body.experimental).toBe(true); + expect(body.method).toBe('semantic'); + expect(Array.isArray(body.data)).toBe(true); + expect(body.data.length).toBe(2); + expect(body.data[0]).toMatchObject({ + id: expect.any(String), + title: expect.any(String), + tags: expect.any(Array), + numUpvotes: expect.any(Number), + numComments: expect.any(Number), + }); + expect(body.pagination).toBeUndefined(); + expect(headers['x-daily-experimental']).toBeDefined(); + }); + + it('should support limit parameter', async () => { + const token = await createTokenForUser(state.con, '5'); + nockMimir(['p1']); + + const { body } = await request(state.app.server) + .get('/public/v1/recommend/semantic') + .query({ q: 'what is the best vector database', limit: 1 }) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(body.data.length).toBeLessThanOrEqual(1); + }); + + it('should return empty data when no results', async () => { + const token = await createTokenForUser(state.con, '5'); + nockMimir([]); + + const { body } = await request(state.app.server) + .get('/public/v1/recommend/semantic') + .query({ q: 'nonexistenttopic' }) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(body.data).toEqual([]); + }); + + it('should require authentication', async () => { + await request(state.app.server) + .get('/public/v1/recommend/semantic') + .query({ q: 'test' }) + .expect(401); + }); +}); diff --git a/src/routes/public/index.ts b/src/routes/public/index.ts index a3e6d37106..3282cb331d 100644 --- a/src/routes/public/index.ts +++ b/src/routes/public/index.ts @@ -13,6 +13,7 @@ import profileRoutes from './profile'; import stackRoutes from './stack'; import experiencesRoutes from './experiences'; import tagsRoutes from './tags'; +import recommendRoutes from './recommend'; import { commonSchemas } from './schemas'; import { PUBLIC_API_PREFIX } from '../../common/constants'; @@ -194,4 +195,5 @@ export default async function ( await fastify.register(stackRoutes, { prefix: '/profile/stack' }); await fastify.register(experiencesRoutes, { prefix: '/profile/experiences' }); await fastify.register(tagsRoutes, { prefix: '/tags' }); + await fastify.register(recommendRoutes, { prefix: '/recommend' }); } diff --git a/src/routes/public/recommend.ts b/src/routes/public/recommend.ts new file mode 100644 index 0000000000..2adf00ba9f --- /dev/null +++ b/src/routes/public/recommend.ts @@ -0,0 +1,227 @@ +import type { FastifyInstance } from 'fastify'; +import { executeGraphql } from './graphqlExecutor'; +import type { FeedConnection, PostNode } from './common'; +import { + parseLimit, + ensureDbConnection, + POST_NODE_FIELDS, + PAGE_INFO_FIELDS, +} from './common'; + +const EXPERIMENTAL_HEADER = 'x-daily-experimental'; +const EXPERIMENTAL_WARNING = + 'This endpoint is experimental and may be removed or changed without notice.'; + +const RECOMMEND_MAX_LIMIT = 20; + +const SEARCH_POSTS_QUERY = ` + query PublicApiRecommend($query: String!, $first: Int, $after: String, $time: SearchTime) { + searchPosts(query: $query, first: $first, after: $after, time: $time) { + edges { + node { + ${POST_NODE_FIELDS} + } + } + ${PAGE_INFO_FIELDS} + } + } +`; + +type SearchPostsResponse = { + searchPosts: FeedConnection; +}; + +const TIME_MAP: Record = { + day: 'Today', + week: 'LastSevenDays', + month: 'LastThirtyDays', + year: 'ThisYear', + all: 'AllTime', +}; + +export default async function (fastify: FastifyInstance): Promise { + // Option 1: Keyword-based recommendation + // Best for: extracted technical terms, specific technology names + fastify.get<{ + Querystring: { q: string; limit?: string; cursor?: string; time?: string }; + }>( + '/keyword', + { + schema: { + description: + '[EXPERIMENTAL] Recommend articles by keyword search. Best when the query contains specific technical terms (e.g. "RAG", "pgvector", "LangChain"). Returns posts with engagement signals for LLM consumption. This endpoint may be removed or changed without notice.', + tags: ['recommend'], + querystring: { + type: 'object', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'Search query — keywords or technical terms (e.g. "RAG vs fine-tuning", "vector database comparison")', + minLength: 1, + }, + limit: { + type: 'integer', + default: 10, + maximum: 20, + minimum: 1, + description: + 'Number of articles to return (1-20, default 10). Kept small for LLM context efficiency.', + }, + cursor: { + type: 'string', + description: 'Pagination cursor from previous response', + }, + time: { + type: 'string', + enum: ['day', 'week', 'month', 'year', 'all'], + description: + 'Time range filter — use "month" or "year" for recent content, "all" for comprehensive results', + }, + }, + }, + response: { + 200: { + type: 'object', + properties: { + experimental: { type: 'boolean' }, + method: { + type: 'string', + description: 'Search method used (keyword)', + }, + data: { type: 'array', items: { $ref: 'FeedPost#' } }, + pagination: { $ref: 'Pagination#' }, + }, + }, + 400: { $ref: 'Error#' }, + 401: { $ref: 'Error#' }, + 429: { $ref: 'RateLimitError#' }, + }, + }, + }, + async (request, reply) => { + const { q, time } = request.query; + const limit = parseLimit(request.query.limit, RECOMMEND_MAX_LIMIT); + const { cursor } = request.query; + const con = ensureDbConnection(fastify.con); + + reply.header(EXPERIMENTAL_HEADER, EXPERIMENTAL_WARNING); + + return executeGraphql( + con, + { + query: SEARCH_POSTS_QUERY, + variables: { + query: q, + first: limit, + after: cursor ?? null, + time: time ? TIME_MAP[time] : null, + }, + }, + (json) => { + const result = json as unknown as SearchPostsResponse; + return { + experimental: true, + method: 'keyword', + data: result.searchPosts.edges.map(({ node }) => node), + pagination: { + hasNextPage: result.searchPosts.pageInfo.hasNextPage, + cursor: result.searchPosts.pageInfo.endCursor, + }, + }; + }, + request, + reply, + ); + }, + ); + + // Option 2: Semantic recommendation via Mimir + // Best for: natural language questions, vague queries from non-technical users + // Uses the same underlying Mimir search but framed for single-shot LLM consumption + fastify.get<{ + Querystring: { q: string; limit?: string; time?: string }; + }>( + '/semantic', + { + schema: { + description: + '[EXPERIMENTAL] Recommend articles by semantic search. Uses AI-powered matching to find articles for natural language questions. Better for non-technical queries like "how do I make my chatbot remember things?" This endpoint may be removed or changed without notice.', + tags: ['recommend'], + querystring: { + type: 'object', + required: ['q'], + properties: { + q: { + type: 'string', + description: + 'Natural language question or topic (e.g. "how do I make my chatbot remember previous conversations?", "what is the best way to handle authentication in a Next.js app?")', + minLength: 1, + }, + limit: { + type: 'integer', + default: 10, + maximum: 20, + minimum: 1, + description: + 'Number of articles to return (1-20, default 10). Kept small for LLM context efficiency.', + }, + time: { + type: 'string', + enum: ['day', 'week', 'month', 'year', 'all'], + description: + 'Time range filter — use "month" or "year" for recent content, "all" for comprehensive results', + }, + }, + }, + response: { + 200: { + type: 'object', + properties: { + experimental: { type: 'boolean' }, + method: { + type: 'string', + description: 'Search method used (semantic)', + }, + data: { type: 'array', items: { $ref: 'FeedPost#' } }, + }, + }, + 400: { $ref: 'Error#' }, + 401: { $ref: 'Error#' }, + 429: { $ref: 'RateLimitError#' }, + }, + }, + }, + async (request, reply) => { + const { q, time } = request.query; + const limit = parseLimit(request.query.limit, RECOMMEND_MAX_LIMIT); + const con = ensureDbConnection(fastify.con); + + reply.header(EXPERIMENTAL_HEADER, EXPERIMENTAL_WARNING); + + return executeGraphql( + con, + { + query: SEARCH_POSTS_QUERY, + variables: { + query: q, + first: limit, + after: null, + time: time ? TIME_MAP[time] : null, + }, + }, + (json) => { + const result = json as unknown as SearchPostsResponse; + return { + experimental: true, + method: 'semantic', + data: result.searchPosts.edges.map(({ node }) => node), + }; + }, + request, + reply, + ); + }, + ); +} From 2d1a910ed7383220b68fc7da30fa84f703956992 Mon Sep 17 00:00:00 2001 From: capJavert Date: Mon, 9 Mar 2026 12:34:46 +0100 Subject: [PATCH 2/4] refactor: remove experimental/method from response body, keep header only Co-Authored-By: Claude Opus 4.6 --- __tests__/routes/public/recommend.ts | 9 ++------- src/routes/public/recommend.ts | 14 -------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/__tests__/routes/public/recommend.ts b/__tests__/routes/public/recommend.ts index d0704622eb..97e54f5e0f 100644 --- a/__tests__/routes/public/recommend.ts +++ b/__tests__/routes/public/recommend.ts @@ -26,8 +26,6 @@ describe('GET /public/v1/recommend/keyword', () => { .set('Authorization', `Bearer ${token}`) .expect(200); - expect(body.experimental).toBe(true); - expect(body.method).toBe('keyword'); expect(Array.isArray(body.data)).toBe(true); expect(body.data.length).toBe(2); expect(body.data[0]).toMatchObject({ @@ -60,14 +58,13 @@ describe('GET /public/v1/recommend/keyword', () => { const token = await createTokenForUser(state.con, '5'); nockMimir(['p1']); - const { body } = await request(state.app.server) + const { headers } = await request(state.app.server) .get('/public/v1/recommend/keyword') .query({ q: 'react', time: 'month' }) .set('Authorization', `Bearer ${token}`) .expect(200); - expect(body.experimental).toBe(true); - expect(body.method).toBe('keyword'); + expect(headers['x-daily-experimental']).toBeDefined(); }); it('should return empty data when no results', async () => { @@ -102,8 +99,6 @@ describe('GET /public/v1/recommend/semantic', () => { .set('Authorization', `Bearer ${token}`) .expect(200); - expect(body.experimental).toBe(true); - expect(body.method).toBe('semantic'); expect(Array.isArray(body.data)).toBe(true); expect(body.data.length).toBe(2); expect(body.data[0]).toMatchObject({ diff --git a/src/routes/public/recommend.ts b/src/routes/public/recommend.ts index 2adf00ba9f..87f540af5c 100644 --- a/src/routes/public/recommend.ts +++ b/src/routes/public/recommend.ts @@ -85,11 +85,6 @@ export default async function (fastify: FastifyInstance): Promise { 200: { type: 'object', properties: { - experimental: { type: 'boolean' }, - method: { - type: 'string', - description: 'Search method used (keyword)', - }, data: { type: 'array', items: { $ref: 'FeedPost#' } }, pagination: { $ref: 'Pagination#' }, }, @@ -122,8 +117,6 @@ export default async function (fastify: FastifyInstance): Promise { (json) => { const result = json as unknown as SearchPostsResponse; return { - experimental: true, - method: 'keyword', data: result.searchPosts.edges.map(({ node }) => node), pagination: { hasNextPage: result.searchPosts.pageInfo.hasNextPage, @@ -179,11 +172,6 @@ export default async function (fastify: FastifyInstance): Promise { 200: { type: 'object', properties: { - experimental: { type: 'boolean' }, - method: { - type: 'string', - description: 'Search method used (semantic)', - }, data: { type: 'array', items: { $ref: 'FeedPost#' } }, }, }, @@ -214,8 +202,6 @@ export default async function (fastify: FastifyInstance): Promise { (json) => { const result = json as unknown as SearchPostsResponse; return { - experimental: true, - method: 'semantic', data: result.searchPosts.edges.map(({ node }) => node), }; }, From c40b09fe746f6bbf1c8e6e2eb7ecf9229213ada6 Mon Sep 17 00:00:00 2001 From: capJavert Date: Mon, 9 Mar 2026 12:38:08 +0100 Subject: [PATCH 3/4] test: assert actual response data in recommend endpoint tests Co-Authored-By: Claude Opus 4.6 --- __tests__/routes/public/recommend.ts | 126 +++++++++++++++++++-------- 1 file changed, 92 insertions(+), 34 deletions(-) diff --git a/__tests__/routes/public/recommend.ts b/__tests__/routes/public/recommend.ts index 97e54f5e0f..61c353f65e 100644 --- a/__tests__/routes/public/recommend.ts +++ b/__tests__/routes/public/recommend.ts @@ -4,6 +4,10 @@ import { setupPublicApiTests, createTokenForUser } from './helpers'; const state = setupPublicApiTests(); +afterEach(() => { + nock.cleanAll(); +}); + const nockMimir = (postIds: string[]) => { nock('http://localhost:7600') .post('/v1/search') @@ -16,7 +20,7 @@ const nockMimir = (postIds: string[]) => { }; describe('GET /public/v1/recommend/keyword', () => { - it('should return keyword search results with experimental flag', async () => { + it('should return posts matching mimir results with correct fields', async () => { const token = await createTokenForUser(state.con, '5'); nockMimir(['p1', 'p2']); @@ -26,48 +30,90 @@ describe('GET /public/v1/recommend/keyword', () => { .set('Authorization', `Bearer ${token}`) .expect(200); - expect(Array.isArray(body.data)).toBe(true); - expect(body.data.length).toBe(2); - expect(body.data[0]).toMatchObject({ - id: expect.any(String), - title: expect.any(String), - tags: expect.any(Array), - numUpvotes: expect.any(Number), - numComments: expect.any(Number), - }); + expect(headers['x-daily-experimental']).toBeDefined(); + expect(body.data).toHaveLength(2); + expect(body.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'p1', + title: 'P1', + url: 'http://p1.com', + image: 'https://daily.dev/image.jpg', + type: 'article', + tags: ['javascript', 'webdev'], + source: expect.objectContaining({ + id: 'a', + name: 'A', + handle: 'a', + }), + }), + expect.objectContaining({ + id: 'p2', + title: 'P2', + url: 'http://p2.com', + source: expect.objectContaining({ + id: 'b', + name: 'B', + }), + }), + ]), + ); expect(body.pagination).toMatchObject({ hasNextPage: expect.any(Boolean), + cursor: expect.any(String), }); - expect(headers['x-daily-experimental']).toBeDefined(); }); - it('should support limit parameter', async () => { + it('should respect limit parameter', async () => { const token = await createTokenForUser(state.con, '5'); - nockMimir(['p1']); + // Mimir receives limit=2 and returns 2 results + nockMimir(['p1', 'p2']); const { body } = await request(state.app.server) .get('/public/v1/recommend/keyword') - .query({ q: 'typescript', limit: 1 }) + .query({ q: 'typescript', limit: 2 }) .set('Authorization', `Bearer ${token}`) .expect(200); - expect(body.data.length).toBeLessThanOrEqual(1); + expect(body.data).toHaveLength(2); + expect(body.data[0]).toMatchObject({ id: 'p1' }); + expect(body.data[1]).toMatchObject({ id: 'p2' }); }); - it('should support time filter', async () => { + it('should pass time filter to search', async () => { const token = await createTokenForUser(state.con, '5'); nockMimir(['p1']); - const { headers } = await request(state.app.server) + const { body, headers } = await request(state.app.server) .get('/public/v1/recommend/keyword') .query({ q: 'react', time: 'month' }) .set('Authorization', `Bearer ${token}`) .expect(200); expect(headers['x-daily-experimental']).toBeDefined(); + expect(body.data).toHaveLength(1); + expect(body.data[0]).toMatchObject({ + id: 'p1', + title: 'P1', + }); }); - it('should return empty data when no results', async () => { + it('should not return private posts', async () => { + const token = await createTokenForUser(state.con, '5'); + // p6 is private + nockMimir(['p1', 'p6']); + + const { body } = await request(state.app.server) + .get('/public/v1/recommend/keyword') + .query({ q: 'test' }) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(body.data).toHaveLength(1); + expect(body.data[0].id).toBe('p1'); + }); + + it('should return empty data when no mimir results', async () => { const token = await createTokenForUser(state.con, '5'); nockMimir([]); @@ -89,7 +135,7 @@ describe('GET /public/v1/recommend/keyword', () => { }); describe('GET /public/v1/recommend/semantic', () => { - it('should return semantic search results with experimental flag', async () => { + it('should return posts matching mimir results with correct fields', async () => { const token = await createTokenForUser(state.con, '5'); nockMimir(['p1', 'p2']); @@ -99,33 +145,45 @@ describe('GET /public/v1/recommend/semantic', () => { .set('Authorization', `Bearer ${token}`) .expect(200); - expect(Array.isArray(body.data)).toBe(true); - expect(body.data.length).toBe(2); - expect(body.data[0]).toMatchObject({ - id: expect.any(String), - title: expect.any(String), - tags: expect.any(Array), - numUpvotes: expect.any(Number), - numComments: expect.any(Number), - }); - expect(body.pagination).toBeUndefined(); expect(headers['x-daily-experimental']).toBeDefined(); + expect(body.data).toHaveLength(2); + expect(body.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'p1', + title: 'P1', + url: 'http://p1.com', + tags: ['javascript', 'webdev'], + source: expect.objectContaining({ id: 'a', name: 'A' }), + }), + expect.objectContaining({ + id: 'p2', + title: 'P2', + url: 'http://p2.com', + source: expect.objectContaining({ id: 'b', name: 'B' }), + }), + ]), + ); + expect(body.pagination).toBeUndefined(); }); - it('should support limit parameter', async () => { + it('should respect limit parameter', async () => { const token = await createTokenForUser(state.con, '5'); - nockMimir(['p1']); + // Mimir receives limit=2 and returns 2 results + nockMimir(['p1', 'p2']); const { body } = await request(state.app.server) .get('/public/v1/recommend/semantic') - .query({ q: 'what is the best vector database', limit: 1 }) + .query({ q: 'what is the best vector database', limit: 2 }) .set('Authorization', `Bearer ${token}`) .expect(200); - expect(body.data.length).toBeLessThanOrEqual(1); + expect(body.data).toHaveLength(2); + expect(body.data[0]).toMatchObject({ id: 'p1' }); + expect(body.data[1]).toMatchObject({ id: 'p2' }); }); - it('should return empty data when no results', async () => { + it('should return empty data when no mimir results', async () => { const token = await createTokenForUser(state.con, '5'); nockMimir([]); From e2f79952254e89ee035e6e70decfe80effb4b7cd Mon Sep 17 00:00:00 2001 From: capJavert Date: Mon, 9 Mar 2026 16:25:24 +0100 Subject: [PATCH 4/4] fix: use distinct GraphQL query names for keyword and semantic recommend endpoints Semantic endpoint no longer passes unused $after variable. Co-Authored-By: Claude Opus 4.6 --- src/routes/public/recommend.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/routes/public/recommend.ts b/src/routes/public/recommend.ts index 87f540af5c..d4510f01dd 100644 --- a/src/routes/public/recommend.ts +++ b/src/routes/public/recommend.ts @@ -14,8 +14,8 @@ const EXPERIMENTAL_WARNING = const RECOMMEND_MAX_LIMIT = 20; -const SEARCH_POSTS_QUERY = ` - query PublicApiRecommend($query: String!, $first: Int, $after: String, $time: SearchTime) { +const KEYWORD_SEARCH_QUERY = ` + query PublicApiRecommendKeyword($query: String!, $first: Int, $after: String, $time: SearchTime) { searchPosts(query: $query, first: $first, after: $after, time: $time) { edges { node { @@ -27,6 +27,19 @@ const SEARCH_POSTS_QUERY = ` } `; +const SEMANTIC_SEARCH_QUERY = ` + query PublicApiRecommendSemantic($query: String!, $first: Int, $time: SearchTime) { + searchPosts(query: $query, first: $first, time: $time) { + edges { + node { + ${POST_NODE_FIELDS} + } + } + ${PAGE_INFO_FIELDS} + } + } +`; + type SearchPostsResponse = { searchPosts: FeedConnection; }; @@ -106,7 +119,7 @@ export default async function (fastify: FastifyInstance): Promise { return executeGraphql( con, { - query: SEARCH_POSTS_QUERY, + query: KEYWORD_SEARCH_QUERY, variables: { query: q, first: limit, @@ -191,11 +204,10 @@ export default async function (fastify: FastifyInstance): Promise { return executeGraphql( con, { - query: SEARCH_POSTS_QUERY, + query: SEMANTIC_SEARCH_QUERY, variables: { query: q, first: limit, - after: null, time: time ? TIME_MAP[time] : null, }, },