Skip to content

feat(commerce): add hybrid search#308

Merged
SAHEED2010 merged 2 commits into
devfrom
feat/search
May 22, 2026
Merged

feat(commerce): add hybrid search#308
SAHEED2010 merged 2 commits into
devfrom
feat/search

Conversation

@SAHEED2010
Copy link
Copy Markdown
Collaborator

@SAHEED2010 SAHEED2010 commented May 22, 2026

What does this PR do?

Implements B-25 — Hybrid Search. This adds a public GET /search endpoint for product, store, and post search with cursor pagination, safe response mappers, Tier 0 exclusion, keyword fallback for public text search, and an internal optional pgvector path for future WhatsApp/image search callers.

Task

B-25 — Hybrid Search: pgvector + tsvector + metadata

What changed

  • Added SearchModule, SearchController, SearchService, and SearchQueryDto
  • Added public GET /search
  • Added product search with keyword fallback
  • Added store search
  • Added post search
  • Added cursor pagination
  • Added safe public response mappers
  • Added Tier 0 exclusion across:
    • product search
    • store search
    • post search
  • Added category, price, in-stock, color, and location filter support where current schema supports it
  • Added internal optional product vector search path using parameterized Prisma.sql
  • Wired SearchModule into CommerceDomainModule

Files changed

  • apps/backend/src/domains/commerce.module.ts
  • apps/backend/src/domains/commerce/search/dto/search-query.dto.ts
  • apps/backend/src/domains/commerce/search/search.controller.ts
  • apps/backend/src/domains/commerce/search/search.module.ts
  • apps/backend/src/domains/commerce/search/search.service.ts

Verification

  • pnpm.cmd run lint — PASS
  • npx.cmd tsc --noEmit — PASS
  • pnpm.cmd run build — PASS

Search behavior

  • Public search uses keyword fallback only
  • Public search does not call VertexClient
  • Public search does not fabricate vectors
  • Public search does not add text-only embeddings
  • Product vector search is available only as an internal optional queryVector path through SearchService.searchProducts({ queryVector })
  • Raw SQL is only used for the internal pgvector path
  • Raw SQL uses parameterized Prisma.sql
  • No unsafe raw SQL interpolation added

Scope control

  • No Prisma migration added
  • No embedding generation added
  • No text-only embedding method added
  • No WhatsApp flow changes added
  • No feed/trending implementation added
  • No frontend changes added
  • No order/payment changes added
  • No unrelated Clawpatch findings patched
  • .env.local was not staged or committed

Type of change

  • New feature
  • Bug fix
  • Refactor / cleanup
  • Database migration included
  • Chore / maintenance
  • Documentation

Area affected

  • Backend
  • Web
  • WhatsApp
  • Shared package
  • Database / Prisma
  • GitHub / CI / infrastructure

How to test this

  1. Call GET /search?q=shirt&type=products.
  2. Call GET /search?q=lagos&type=stores.
  3. Call GET /search?q=style&type=posts.
  4. Call GET /search?q=shirt&type=all.
  5. Test product filters such as category, minPrice, maxPrice, and inStock.
  6. Confirm cursor pagination returns stable nextCursor values.
  7. Confirm Tier 0 stores and their attached products/posts are excluded.

Expected result:

  • Product, store, and post search return public-safe results.
  • Tier 0 records are excluded.
  • Public search works without query vectors.
  • Invalid kobo values return controlled 400 errors.
  • No private/internal fields are returned.

Pre-commit checklist

  • Backend lint/type/build pass when backend is affected
  • Web lint/type/build pass when web is affected
  • Shared package build passes when shared is affected
  • No console.log left in production code
  • No secrets or .env files committed
  • No new any types added
  • No non-MVP legacy features reintroduced
  • All money values are BigInt kobo, never float
  • Paystack webhook changes verify HMAC before processing
  • Database migrations are Prisma migrations, not db push
  • Database migrations are backward-compatible or risk is documented

Screenshots

N/A — backend-only PR. No UI changes.

Notes for reviewer

Because the current VertexClient only supports generateEmbedding(imageBuffer, text), public text search intentionally uses keyword fallback and does not generate query vectors at request time. The vector path is implemented as an internal optional service path for future WhatsApp/image search callers that already have a valid query vector.

Summary by CodeRabbit

  • New Features
    • Added a unified search API endpoint supporting searches across products, stores, and posts
    • Implemented advanced filtering options including price range, color, category, location, and stock availability
    • Added pagination support with cursor-based navigation for search results
    • Introduced relevance-based result ranking to improve search quality

Review Change Stack

@SAHEED2010 SAHEED2010 requested a review from onerandomdevv as a code owner May 22, 2026 03:37
@codesandbox
Copy link
Copy Markdown

codesandbox Bot commented May 22, 2026

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Warning

Rate limit exceeded

@SAHEED2010 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 58 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0304f202-5d9f-4274-957b-ff9612d39d70

📥 Commits

Reviewing files that changed from the base of the PR and between 41a6f6c and f312606.

📒 Files selected for processing (1)
  • apps/backend/src/domains/commerce/search/search.service.ts
📝 Walkthrough

Walkthrough

This pull request introduces a new search feature for the commerce domain. It adds a search module with request validation, a controller, and a service that implements paginated search across products (via keyword and vector-based ranking), stores, and posts. The service normalizes inputs, handles cursor-based pagination, and returns strongly-typed results.

Changes

Commerce Search Feature

Layer / File(s) Summary
Search API Contract & Handler
apps/backend/src/domains/commerce/search/dto/search-query.dto.ts, apps/backend/src/domains/commerce/search/search.controller.ts, apps/backend/src/domains/commerce/search/search.service.ts (types)
Defines SearchQueryDto with optional validated fields (q, type, category, minPrice, maxPrice, color, location, inStock, cursor, limit) and search result type enums. Exports response type contracts (SearchPage, GroupedSearchResponse, SingleSearchResponse, ProductSearchResult, StoreSearchResult, PostSearchResult). Introduces SearchController with a GET handler binding query parameters to the DTO and delegating to SearchService.
Search Service Module & Dispatcher
apps/backend/src/domains/commerce/search/search.module.ts, apps/backend/src/domains/commerce/search/search.service.ts (setup & dispatch)
Introduces SearchModule that wires SearchController and exports SearchService. Defines Prisma select projections for product, store, and post entities. Implements SearchService dispatcher that routes requests by search type, delegating to searchProducts(), searchStores(), and searchPosts() entry points.
Product Search - Keyword & Vector
apps/backend/src/domains/commerce/search/search.service.ts (product search)
Implements productWhere builder that enforces status, moderation, tier, and open-store constraints, plus optional filters for location, category, price range, color variants, and stock. Implements searchProductsVector() using raw SQL with pgvector cosine distance, combining vector similarity with conditional keyword and tier boosts, and cursor-based pagination ordered by score then id.
Search Utilities & Response Mapping
apps/backend/src/domains/commerce/search/search.service.ts (utilities, mapping)
Provides query normalization (kobo price range parsing, text trimming, hashtag normalization, boolean parsing), cursor encoding/decoding with type validation, result slicing, and mapping helpers that transform product/store/post records into response types with bigint-to-string price conversion, ISO timestamp formatting, and conditional author/tagged-product inclusion based on product tier and status constraints.
Domain Integration
apps/backend/src/domains/commerce.module.ts
Registers SearchModule in the CommerceDomainModule imports array alongside existing commerce modules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • onerandomdevv

Poem

🐰 A search is born, from vectors to keywords so bright,
Ranking products and posts, stores in sight,
With cursors for pages and filters that gleam,
The commerce domain fulfills a rabbit's dream!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(commerce): add hybrid search' directly and clearly identifies the main change—adding hybrid search functionality to the commerce domain.
Description check ✅ Passed The PR description comprehensively covers all required template sections including purpose, type of change, area affected, testing steps, and pre-commit checklist with detailed verification provided.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/search

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/backend/src/domains/commerce/search/search.service.ts (1)

292-296: ⚡ Quick win

Run grouped searches in parallel for type=all.

These three queries are independent; sequential awaits add avoidable latency.

♻️ Suggested change
-    return {
-      products: await this.searchProducts({ ...params, cursor: null }),
-      stores: await this.searchStores({ ...params, cursor: null }),
-      posts: await this.searchPosts({ ...params, cursor: null }),
-    };
+    const [products, stores, posts] = await Promise.all([
+      this.searchProducts({ ...params, cursor: null }),
+      this.searchStores({ ...params, cursor: null }),
+      this.searchPosts({ ...params, cursor: null }),
+    ]);
+    return { products, stores, posts };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/domains/commerce/search/search.service.ts` around lines 292
- 296, The current implementation performs three independent searches
sequentially (searchProducts, searchStores, searchPosts) which increases
latency; change the logic for the type=all branch to run these calls in parallel
using Promise.all (or equivalent), await the combined promise once, and then map
the returned tuple into the response object so products, stores, and posts are
all filled from the parallel results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/domains/commerce/search/dto/search-query.dto.ts`:
- Around line 59-61: Rename the boolean query flag property in SearchQueryDto
from inStock to isInStock: update the property name (`@IsOptional`()
`@IsBooleanString`() inStock?: string) to isInStock and update all usages and
service contracts that accept or map this DTO (search services, controllers, and
any mappers/validators) so the DTO, method signatures, parameter names, and
downstream mappings consistently use isInStock; also update any tests and API
docs to reflect the new field name.

In `@apps/backend/src/domains/commerce/search/search.service.ts`:
- Around line 19-23: The SearchCursor interface (and other new cursor/result
interfaces) use a bare field name "id" which violates the project rule requiring
ID fields be suffixed with "Id"; rename the field in SearchCursor (and the other
introduced interfaces referenced in the comment) from id to a descriptive
<Entity>Id (e.g., productId, storeId, etc.), then update all code that
constructs, serializes/deserializes or reads these objects (e.g., any cursor
builders/parsers, search result mappers, and repository mapping functions) to
use the new <Entity>Id names; ensure tests and any JSON payload keys are updated
accordingly so callers and consumers use the new *Id names consistently.
- Around line 338-351: searchStores builds textFilter and accepts a normalized
params.location but never applies it to the Prisma query; update the where
passed to this.prisma.storeProfile.findMany (inside searchStores) to include a
location-based condition when params.location (or the normalized location
variable) is present—e.g. add a property like businessLocation / location: {
equals: params.location } or contains/match the normalized value depending on
your schema—so the location filter is combined with tier/isOpen/textFilter in
the where clause.
- Around line 705-715: The parseKobo function currently accepts arbitrarily
large BigInt values which can later fail at DB cast time; update parseKobo to
validate an upper bound before returning: after parsing to BigInt in parseKobo,
compare against a MAX_KOBO constant (e.g. 9223372036854775807n for signed
64-bit) and throw a clear error if parsed > MAX_KOBO (and keep the existing
negative check), so callers can produce a clean 400 rather than letting the DB
error; reference the parseKobo function and add a MAX_KOBO constant near it.

---

Nitpick comments:
In `@apps/backend/src/domains/commerce/search/search.service.ts`:
- Around line 292-296: The current implementation performs three independent
searches sequentially (searchProducts, searchStores, searchPosts) which
increases latency; change the logic for the type=all branch to run these calls
in parallel using Promise.all (or equivalent), await the combined promise once,
and then map the returned tuple into the response object so products, stores,
and posts are all filled from the parallel results.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b016643f-5a16-418c-a03c-25141791d735

📥 Commits

Reviewing files that changed from the base of the PR and between 774ec71 and 41a6f6c.

📒 Files selected for processing (5)
  • apps/backend/src/domains/commerce.module.ts
  • apps/backend/src/domains/commerce/search/dto/search-query.dto.ts
  • apps/backend/src/domains/commerce/search/search.controller.ts
  • apps/backend/src/domains/commerce/search/search.module.ts
  • apps/backend/src/domains/commerce/search/search.service.ts

Comment on lines +59 to +61
@IsOptional()
@IsBooleanString()
inStock?: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Rename boolean query flag to follow the API naming convention.

inStock should be renamed to isInStock (and propagated through service contracts) to match the backend boolean-field rule.

As per coding guidelines: "All boolean field names must be prefixed with 'is', 'has', 'can', or 'should'."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/domains/commerce/search/dto/search-query.dto.ts` around
lines 59 - 61, Rename the boolean query flag property in SearchQueryDto from
inStock to isInStock: update the property name (`@IsOptional`() `@IsBooleanString`()
inStock?: string) to isInStock and update all usages and service contracts that
accept or map this DTO (search services, controllers, and any
mappers/validators) so the DTO, method signatures, parameter names, and
downstream mappings consistently use isInStock; also update any tests and API
docs to reflect the new field name.

Comment on lines +19 to +23
interface SearchCursor {
type: SingleSearchType;
id: string;
score?: number;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Use *Id names in new contracts instead of bare id.

The new cursor/result interfaces introduce multiple id fields that don’t follow the repository naming rule (...Id).

As per coding guidelines: "All ID field names must be suffixed with 'Id' (e.g. storeId, productId, orderId)."

Also applies to: 65-67, 83-84, 95-96, 99-101, 106-107, 114-115

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/domains/commerce/search/search.service.ts` around lines 19 -
23, The SearchCursor interface (and other new cursor/result interfaces) use a
bare field name "id" which violates the project rule requiring ID fields be
suffixed with "Id"; rename the field in SearchCursor (and the other introduced
interfaces referenced in the comment) from id to a descriptive <Entity>Id (e.g.,
productId, storeId, etc.), then update all code that constructs,
serializes/deserializes or reads these objects (e.g., any cursor
builders/parsers, search result mappers, and repository mapping functions) to
use the new <Entity>Id names; ensure tests and any JSON payload keys are updated
accordingly so callers and consumers use the new *Id names consistently.

Comment thread apps/backend/src/domains/commerce/search/search.service.ts
Comment thread apps/backend/src/domains/commerce/search/search.service.ts
@SAHEED2010 SAHEED2010 merged commit 096887b into dev May 22, 2026
8 checks passed
@SAHEED2010 SAHEED2010 deleted the feat/search branch May 22, 2026 03:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants