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 f81b67860b
commit f2dad45c7c
2 changed files with 22 additions and 4 deletions

View File

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

View File

@ -8,6 +8,7 @@
*/
import { Router, Request, Response } from "express";
import { semanticSearch, getCollectionInfo, CollectionName } from "../embeddings/client";
import { searchTransceivers } from "../db/queries";
export const searchRouter = Router();
@ -43,11 +44,20 @@ searchRouter.get("/", async (req: Request, res: Response) => {
}
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({
success: true,
query,
collection,
fallback: usedFallback ? "fts" : undefined,
results: results.map((r) => ({
id: r.id,
score: Math.round(r.score * 1000) / 1000,
@ -56,6 +66,14 @@ searchRouter.get("/", async (req: Request, res: Response) => {
count: results.length,
});
} 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({
success: false,
error: "Vector search unavailable",