bluera-knowledge 0.9.36 → 0.9.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/{chunk-Z2KKVH45.js → chunk-6TKD5XE4.js} +2 -2
- package/dist/{chunk-DC7CGSGT.js → chunk-AT6G626F.js} +2 -2
- package/dist/{chunk-WFNPNAAP.js → chunk-CGDEV2RC.js} +59 -29
- package/dist/chunk-CGDEV2RC.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/mcp/server.js +2 -2
- package/dist/workers/background-worker-cli.js +2 -2
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/db/lance.ts +13 -19
- package/src/services/search.service.test.ts +209 -0
- package/src/services/search.service.ts +77 -19
- package/tests/integration/search-quality.test.ts +5 -3
- package/dist/chunk-WFNPNAAP.js.map +0 -1
- /package/dist/{chunk-Z2KKVH45.js.map → chunk-6TKD5XE4.js.map} +0 -0
- /package/dist/{chunk-DC7CGSGT.js.map → chunk-AT6G626F.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runMCPServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6TKD5XE4.js";
|
|
5
5
|
import {
|
|
6
6
|
IntelligentCrawler
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AT6G626F.js";
|
|
8
8
|
import {
|
|
9
9
|
ASTParser,
|
|
10
10
|
ChunkingService,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
err,
|
|
17
17
|
extractRepoName,
|
|
18
18
|
ok
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-CGDEV2RC.js";
|
|
20
20
|
import "./chunk-6FHWC36B.js";
|
|
21
21
|
|
|
22
22
|
// src/index.ts
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
IntelligentCrawler
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-AT6G626F.js";
|
|
5
5
|
import {
|
|
6
6
|
JobService,
|
|
7
7
|
createDocumentId,
|
|
8
8
|
createServices,
|
|
9
9
|
createStoreId
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-CGDEV2RC.js";
|
|
11
11
|
import "../chunk-6FHWC36B.js";
|
|
12
12
|
|
|
13
13
|
// src/workers/background-worker.ts
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/src/db/lance.ts
CHANGED
|
@@ -74,33 +74,27 @@ export class LanceStore {
|
|
|
74
74
|
storeId: StoreId,
|
|
75
75
|
vector: number[],
|
|
76
76
|
limit: number,
|
|
77
|
-
threshold
|
|
77
|
+
// threshold is kept for API compatibility but filtering is done after normalization
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
79
|
+
_threshold?: number
|
|
78
80
|
): Promise<
|
|
79
81
|
Array<{ id: DocumentId; content: string; score: number; metadata: DocumentMetadata }>
|
|
80
82
|
> {
|
|
81
83
|
const table = await this.getTable(storeId);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (threshold !== undefined) {
|
|
85
|
-
query = query.distanceType('cosine');
|
|
86
|
-
}
|
|
84
|
+
const query = table.vectorSearch(vector).limit(limit).distanceType('cosine');
|
|
87
85
|
|
|
88
86
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
89
87
|
const results = (await query.toArray()) as SearchHit[];
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
score: 1 - r._distance,
|
|
101
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
102
|
-
metadata: JSON.parse(r.metadata) as DocumentMetadata,
|
|
103
|
-
}));
|
|
89
|
+
// Return all results - threshold filtering is applied after score normalization
|
|
90
|
+
// in search.service.ts to match displayed scores
|
|
91
|
+
return results.map((r) => ({
|
|
92
|
+
id: createDocumentId(r.id),
|
|
93
|
+
content: r.content,
|
|
94
|
+
score: 1 - r._distance,
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
96
|
+
metadata: JSON.parse(r.metadata) as DocumentMetadata,
|
|
97
|
+
}));
|
|
104
98
|
}
|
|
105
99
|
|
|
106
100
|
async createFtsIndex(storeId: StoreId): Promise<void> {
|
|
@@ -1783,3 +1783,212 @@ describe('SearchService - Code Graph Integration', () => {
|
|
|
1783
1783
|
expect(results.results[0]?.full?.relatedCode?.length).toBe(10);
|
|
1784
1784
|
});
|
|
1785
1785
|
});
|
|
1786
|
+
|
|
1787
|
+
describe('SearchService - Threshold Filtering', () => {
|
|
1788
|
+
let mockLanceStore: LanceStore;
|
|
1789
|
+
let mockEmbeddingEngine: EmbeddingEngine;
|
|
1790
|
+
let searchService: SearchService;
|
|
1791
|
+
const storeId = createStoreId('test-store');
|
|
1792
|
+
|
|
1793
|
+
beforeEach(() => {
|
|
1794
|
+
mockLanceStore = {
|
|
1795
|
+
search: vi.fn(),
|
|
1796
|
+
fullTextSearch: vi.fn(),
|
|
1797
|
+
} as unknown as LanceStore;
|
|
1798
|
+
|
|
1799
|
+
mockEmbeddingEngine = {
|
|
1800
|
+
embed: vi.fn().mockResolvedValue([0.1, 0.2, 0.3]),
|
|
1801
|
+
} as unknown as EmbeddingEngine;
|
|
1802
|
+
|
|
1803
|
+
searchService = new SearchService(mockLanceStore, mockEmbeddingEngine);
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
it('applies threshold to normalized scores, not raw scores', async () => {
|
|
1807
|
+
// Setup: 3 results with different raw scores
|
|
1808
|
+
// In hybrid mode with RRF, ranks matter more than raw scores
|
|
1809
|
+
// doc1 appears in both vector and FTS -> highest RRF score -> normalized to 1.0
|
|
1810
|
+
// doc2 appears only in vector -> middle RRF score -> normalized to ~0.5
|
|
1811
|
+
// doc3 appears only in vector, lowest rank -> lowest RRF score -> normalized to 0.0
|
|
1812
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
1813
|
+
{
|
|
1814
|
+
id: createDocumentId('doc1'),
|
|
1815
|
+
score: 0.9,
|
|
1816
|
+
content: 'result 1',
|
|
1817
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
id: createDocumentId('doc2'),
|
|
1821
|
+
score: 0.7,
|
|
1822
|
+
content: 'result 2',
|
|
1823
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1824
|
+
},
|
|
1825
|
+
{
|
|
1826
|
+
id: createDocumentId('doc3'),
|
|
1827
|
+
score: 0.5,
|
|
1828
|
+
content: 'result 3',
|
|
1829
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1830
|
+
},
|
|
1831
|
+
]);
|
|
1832
|
+
// Add doc1 and doc2 to FTS results so they both have good RRF scores
|
|
1833
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([
|
|
1834
|
+
{
|
|
1835
|
+
id: createDocumentId('doc1'),
|
|
1836
|
+
score: 0.9,
|
|
1837
|
+
content: 'result 1',
|
|
1838
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
id: createDocumentId('doc2'),
|
|
1842
|
+
score: 0.7,
|
|
1843
|
+
content: 'result 2',
|
|
1844
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1845
|
+
},
|
|
1846
|
+
]);
|
|
1847
|
+
|
|
1848
|
+
// With threshold 0.4, doc1 (1.0) and doc2 (~0.47) should pass
|
|
1849
|
+
// doc3 (0.0) should be filtered out
|
|
1850
|
+
const results = await searchService.search({
|
|
1851
|
+
query: 'test query',
|
|
1852
|
+
stores: [storeId],
|
|
1853
|
+
mode: 'hybrid',
|
|
1854
|
+
limit: 10,
|
|
1855
|
+
threshold: 0.4,
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
// Should return 2 results: scores >= 0.4 (normalized)
|
|
1859
|
+
expect(results.results.length).toBe(2);
|
|
1860
|
+
expect(results.results[0]?.id).toBe(createDocumentId('doc1'));
|
|
1861
|
+
expect(results.results[1]?.id).toBe(createDocumentId('doc2'));
|
|
1862
|
+
|
|
1863
|
+
// Verify normalized scores
|
|
1864
|
+
expect(results.results[0]?.score).toBe(1.0);
|
|
1865
|
+
expect(results.results[1]?.score).toBeGreaterThanOrEqual(0.4);
|
|
1866
|
+
|
|
1867
|
+
// Verify doc3 was filtered out (its normalized score is 0.0)
|
|
1868
|
+
expect(results.results.find((r) => r.id === createDocumentId('doc3'))).toBeUndefined();
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
it('returns all results when threshold is 0', async () => {
|
|
1872
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
1873
|
+
{
|
|
1874
|
+
id: createDocumentId('doc1'),
|
|
1875
|
+
score: 0.9,
|
|
1876
|
+
content: 'result 1',
|
|
1877
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
id: createDocumentId('doc2'),
|
|
1881
|
+
score: 0.1,
|
|
1882
|
+
content: 'result 2',
|
|
1883
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1884
|
+
},
|
|
1885
|
+
]);
|
|
1886
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
1887
|
+
|
|
1888
|
+
const results = await searchService.search({
|
|
1889
|
+
query: 'test query',
|
|
1890
|
+
stores: [storeId],
|
|
1891
|
+
mode: 'hybrid',
|
|
1892
|
+
limit: 10,
|
|
1893
|
+
threshold: 0,
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
// All results should be returned (scores >= 0)
|
|
1897
|
+
expect(results.results.length).toBe(2);
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
it('returns no results when threshold is higher than all scores', async () => {
|
|
1901
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
1902
|
+
{
|
|
1903
|
+
id: createDocumentId('doc1'),
|
|
1904
|
+
score: 0.9,
|
|
1905
|
+
content: 'result 1',
|
|
1906
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
id: createDocumentId('doc2'),
|
|
1910
|
+
score: 0.8,
|
|
1911
|
+
content: 'result 2',
|
|
1912
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1913
|
+
},
|
|
1914
|
+
]);
|
|
1915
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
1916
|
+
|
|
1917
|
+
// Threshold > 1.0 means no results pass
|
|
1918
|
+
const results = await searchService.search({
|
|
1919
|
+
query: 'test query',
|
|
1920
|
+
stores: [storeId],
|
|
1921
|
+
mode: 'hybrid',
|
|
1922
|
+
limit: 10,
|
|
1923
|
+
threshold: 1.1,
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
expect(results.results.length).toBe(0);
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
it('applies threshold in vector mode after score calculation', async () => {
|
|
1930
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
1931
|
+
{
|
|
1932
|
+
id: createDocumentId('doc1'),
|
|
1933
|
+
score: 0.9,
|
|
1934
|
+
content: 'result 1',
|
|
1935
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1936
|
+
},
|
|
1937
|
+
{
|
|
1938
|
+
id: createDocumentId('doc2'),
|
|
1939
|
+
score: 0.3,
|
|
1940
|
+
content: 'result 2',
|
|
1941
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1942
|
+
},
|
|
1943
|
+
]);
|
|
1944
|
+
|
|
1945
|
+
const results = await searchService.search({
|
|
1946
|
+
query: 'test query',
|
|
1947
|
+
stores: [storeId],
|
|
1948
|
+
mode: 'vector',
|
|
1949
|
+
limit: 10,
|
|
1950
|
+
threshold: 0.5,
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
// Only doc1 should pass (normalized score 1.0 >= 0.5)
|
|
1954
|
+
// doc2 has normalized score 0.0 which is < 0.5
|
|
1955
|
+
expect(results.results.length).toBe(1);
|
|
1956
|
+
expect(results.results[0]?.id).toBe(createDocumentId('doc1'));
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
it('maintains correct result count metadata after threshold filtering', async () => {
|
|
1960
|
+
vi.mocked(mockLanceStore.search).mockResolvedValue([
|
|
1961
|
+
{
|
|
1962
|
+
id: createDocumentId('doc1'),
|
|
1963
|
+
score: 0.9,
|
|
1964
|
+
content: 'result 1',
|
|
1965
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1966
|
+
},
|
|
1967
|
+
{
|
|
1968
|
+
id: createDocumentId('doc2'),
|
|
1969
|
+
score: 0.5,
|
|
1970
|
+
content: 'result 2',
|
|
1971
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
id: createDocumentId('doc3'),
|
|
1975
|
+
score: 0.1,
|
|
1976
|
+
content: 'result 3',
|
|
1977
|
+
metadata: { type: 'file' as const, storeId, indexedAt: new Date() },
|
|
1978
|
+
},
|
|
1979
|
+
]);
|
|
1980
|
+
vi.mocked(mockLanceStore.fullTextSearch).mockResolvedValue([]);
|
|
1981
|
+
|
|
1982
|
+
const results = await searchService.search({
|
|
1983
|
+
query: 'test query',
|
|
1984
|
+
stores: [storeId],
|
|
1985
|
+
mode: 'hybrid',
|
|
1986
|
+
limit: 10,
|
|
1987
|
+
threshold: 0.5,
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Check response metadata
|
|
1991
|
+
expect(results.totalResults).toBe(results.results.length);
|
|
1992
|
+
expect(results.query).toBe('test query');
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
@@ -370,6 +370,48 @@ export class SearchService {
|
|
|
370
370
|
return queryTerms.filter((term) => lowerContent.includes(term)).length;
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Normalize scores to 0-1 range and optionally filter by threshold.
|
|
375
|
+
* This ensures threshold values match displayed scores (UX consistency).
|
|
376
|
+
*
|
|
377
|
+
* Edge case handling:
|
|
378
|
+
* - If there's only 1 result or all results have the same score, normalization
|
|
379
|
+
* would make them all 1.0. In this case, we keep the raw scores to allow
|
|
380
|
+
* threshold filtering to work meaningfully on absolute quality.
|
|
381
|
+
*/
|
|
382
|
+
private normalizeAndFilterScores(results: SearchResult[], threshold?: number): SearchResult[] {
|
|
383
|
+
if (results.length === 0) return [];
|
|
384
|
+
|
|
385
|
+
// Sort by score descending
|
|
386
|
+
const sorted = [...results].sort((a, b) => b.score - a.score);
|
|
387
|
+
|
|
388
|
+
// Get score range for normalization
|
|
389
|
+
const first = sorted[0];
|
|
390
|
+
const last = sorted[sorted.length - 1];
|
|
391
|
+
if (first === undefined || last === undefined) return [];
|
|
392
|
+
|
|
393
|
+
const maxScore = first.score;
|
|
394
|
+
const minScore = last.score;
|
|
395
|
+
const range = maxScore - minScore;
|
|
396
|
+
|
|
397
|
+
// Only normalize when there's meaningful score variation
|
|
398
|
+
// If all scores are the same (range = 0), keep raw scores for threshold filtering
|
|
399
|
+
const normalized =
|
|
400
|
+
range > 0
|
|
401
|
+
? sorted.map((r) => ({
|
|
402
|
+
...r,
|
|
403
|
+
score: Math.round(((r.score - minScore) / range) * 1000000) / 1000000,
|
|
404
|
+
}))
|
|
405
|
+
: sorted; // Keep raw scores when no variation (allows threshold to filter by quality)
|
|
406
|
+
|
|
407
|
+
// Apply threshold filter on scores
|
|
408
|
+
if (threshold !== undefined) {
|
|
409
|
+
return normalized.filter((r) => r.score >= threshold);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return normalized;
|
|
413
|
+
}
|
|
414
|
+
|
|
373
415
|
private async vectorSearch(
|
|
374
416
|
query: string,
|
|
375
417
|
stores: readonly StoreId[],
|
|
@@ -391,7 +433,9 @@ export class SearchService {
|
|
|
391
433
|
);
|
|
392
434
|
}
|
|
393
435
|
|
|
394
|
-
|
|
436
|
+
// Normalize scores and apply threshold filter
|
|
437
|
+
const normalized = this.normalizeAndFilterScores(results, threshold);
|
|
438
|
+
return normalized.slice(0, limit);
|
|
395
439
|
}
|
|
396
440
|
|
|
397
441
|
private async ftsSearch(
|
|
@@ -425,9 +469,9 @@ export class SearchService {
|
|
|
425
469
|
// Classify query intents for context-aware ranking (supports multiple intents)
|
|
426
470
|
const intents = classifyQueryIntents(query);
|
|
427
471
|
|
|
428
|
-
// Get both result sets
|
|
472
|
+
// Get both result sets (don't pass threshold - apply after RRF normalization)
|
|
429
473
|
const [vectorResults, ftsResults] = await Promise.all([
|
|
430
|
-
this.vectorSearch(query, stores, limit * 2
|
|
474
|
+
this.vectorSearch(query, stores, limit * 2),
|
|
431
475
|
this.ftsSearch(query, stores, limit * 2),
|
|
432
476
|
]);
|
|
433
477
|
|
|
@@ -534,34 +578,48 @@ export class SearchService {
|
|
|
534
578
|
const sorted = rrfScores.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
535
579
|
|
|
536
580
|
// Normalize scores to 0-1 range for better interpretability
|
|
581
|
+
let normalizedResults: SearchResult[];
|
|
582
|
+
|
|
537
583
|
if (sorted.length > 0) {
|
|
538
584
|
const first = sorted[0];
|
|
539
585
|
const last = sorted[sorted.length - 1];
|
|
540
586
|
if (first === undefined || last === undefined) {
|
|
541
|
-
|
|
587
|
+
normalizedResults = sorted.map((r) => ({
|
|
542
588
|
...r.result,
|
|
543
589
|
score: r.score,
|
|
544
590
|
rankingMetadata: r.metadata,
|
|
545
591
|
}));
|
|
592
|
+
} else {
|
|
593
|
+
const maxScore = first.score;
|
|
594
|
+
const minScore = last.score;
|
|
595
|
+
const range = maxScore - minScore;
|
|
596
|
+
|
|
597
|
+
if (range > 0) {
|
|
598
|
+
// Round to avoid floating point precision issues in threshold comparisons
|
|
599
|
+
normalizedResults = sorted.map((r) => ({
|
|
600
|
+
...r.result,
|
|
601
|
+
score: Math.round(((r.score - minScore) / range) * 1000000) / 1000000,
|
|
602
|
+
rankingMetadata: r.metadata,
|
|
603
|
+
}));
|
|
604
|
+
} else {
|
|
605
|
+
// All same score - keep raw scores (allows threshold to filter by quality)
|
|
606
|
+
normalizedResults = sorted.map((r) => ({
|
|
607
|
+
...r.result,
|
|
608
|
+
score: r.score,
|
|
609
|
+
rankingMetadata: r.metadata,
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
546
612
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
613
|
+
} else {
|
|
614
|
+
normalizedResults = [];
|
|
615
|
+
}
|
|
550
616
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
score: (r.score - minScore) / range,
|
|
555
|
-
rankingMetadata: r.metadata,
|
|
556
|
-
}));
|
|
557
|
-
}
|
|
617
|
+
// Apply threshold filter on normalized scores (UX consistency)
|
|
618
|
+
if (threshold !== undefined) {
|
|
619
|
+
return normalizedResults.filter((r) => r.score >= threshold);
|
|
558
620
|
}
|
|
559
621
|
|
|
560
|
-
return
|
|
561
|
-
...r.result,
|
|
562
|
-
score: r.score,
|
|
563
|
-
rankingMetadata: r.metadata,
|
|
564
|
-
}));
|
|
622
|
+
return normalizedResults;
|
|
565
623
|
}
|
|
566
624
|
|
|
567
625
|
async searchAllStores(query: SearchQuery, storeIds: StoreId[]): Promise<SearchResponse> {
|
|
@@ -561,7 +561,8 @@ export function authMiddleware(req: Request, res: Response, next: Next) {
|
|
|
561
561
|
|
|
562
562
|
describe('Edge Cases', () => {
|
|
563
563
|
it('handles queries with no results gracefully', async () => {
|
|
564
|
-
//
|
|
564
|
+
// Semantic search may return results even for nonsense queries (nearest neighbors)
|
|
565
|
+
// With normalized scores, threshold filtering applies to relative scores
|
|
565
566
|
const response = await searchService.search({
|
|
566
567
|
query: 'xyznonexistent123',
|
|
567
568
|
threshold: 0.9,
|
|
@@ -569,8 +570,9 @@ export function authMiddleware(req: Request, res: Response, next: Next) {
|
|
|
569
570
|
});
|
|
570
571
|
const results = adaptApiResults(response.results);
|
|
571
572
|
|
|
572
|
-
//
|
|
573
|
-
|
|
573
|
+
// Search should not throw and may return some results
|
|
574
|
+
// (embedding models find nearest neighbors even for gibberish)
|
|
575
|
+
expect(Array.isArray(results)).toBe(true);
|
|
574
576
|
});
|
|
575
577
|
|
|
576
578
|
it('handles special characters in queries', async () => {
|