fix(api): part-number ILIKE search + verified-first catalog ordering + FTS-primary product search

This commit is contained in:
Rene Fichtmueller 2026-06-04 10:11:37 +00:00
parent 9979b79434
commit d6da7aa94c
2 changed files with 22 additions and 4 deletions

View File

@ -28,7 +28,7 @@ export async function searchTransceivers(params: SearchParams) {
let idx = 1; let idx = 1;
if (params.q) { if (params.q) {
conditions.push(`search_vector @@ plainto_tsquery('english', $${idx})`); conditions.push(`(search_vector @@ plainto_tsquery('english', $${idx}) OR t.part_number ILIKE '%' || $${idx} || '%' OR t.standard_name ILIKE '%' || $${idx} || '%')`);
values.push(params.q); values.push(params.q);
idx++; idx++;
} }
@ -98,8 +98,8 @@ export async function searchTransceivers(params: SearchParams) {
// Add relevance ranking when full-text search is used // Add relevance ranking when full-text search is used
const orderBy = params.q const orderBy = params.q
? `ORDER BY ts_rank(search_vector, plainto_tsquery('english', $1)) DESC` ? `ORDER BY (t.part_number ILIKE $1) DESC, ts_rank(search_vector, plainto_tsquery('english', $1)) DESC, fully_verified DESC NULLS LAST, has_image DESC NULLS LAST`
: `ORDER BY speed_gbps DESC, reach_meters ASC`; : `ORDER BY fully_verified DESC NULLS LAST, has_image DESC NULLS LAST, speed_gbps DESC NULLS LAST, reach_meters ASC NULLS LAST`;
const query = ` const query = `
SELECT t.*, v.name as vendor_name SELECT t.*, v.name as vendor_name

View File

@ -8,6 +8,7 @@
*/ */
import { Router, Request, Response } from "express"; import { Router, Request, Response } from "express";
import { semanticSearch, getCollectionInfo, CollectionName } from "../embeddings/client"; import { semanticSearch, getCollectionInfo, CollectionName } from "../embeddings/client";
import { searchTransceivers } from "../db/queries";
export const searchRouter = Router(); export const searchRouter = Router();
@ -43,11 +44,20 @@ searchRouter.get("/", async (req: Request, res: Response) => {
} }
try { try {
const results = await semanticSearch(collection, query, limit); let results: any[];
let usedFallback = false;
if (collection === "product_embeddings") {
const fts = await searchTransceivers({ q: query, limit });
results = (((fts as any).data) || []).map((t: any) => ({ id: t.id, score: 0.5, payload: t }));
usedFallback = true;
} else {
results = await semanticSearch(collection, query, limit);
}
res.json({ res.json({
success: true, success: true,
query, query,
collection, collection,
fallback: usedFallback ? "fts" : undefined,
results: results.map((r) => ({ results: results.map((r) => ({
id: r.id, id: r.id,
score: Math.round(r.score * 1000) / 1000, score: Math.round(r.score * 1000) / 1000,
@ -56,6 +66,14 @@ searchRouter.get("/", async (req: Request, res: Response) => {
count: results.length, count: results.length,
}); });
} catch (err) { } catch (err) {
if (collection === "product_embeddings") {
try {
const fts = await searchTransceivers({ q: query, limit });
const results = (((fts as any).data) || []).map((t: any) => ({ id: t.id, score: 0.5, ...t }));
res.json({ success: true, query, collection, fallback: "fts", results, count: results.length });
return;
} catch (e2) { /* fall through */ }
}
res.status(503).json({ res.status(503).json({
success: false, success: false,
error: "Vector search unavailable", error: "Vector search unavailable",