Skip to content

Commit 512119d

Browse files
committed
fix(core): anchor compound lexical identifiers
Release highlights: - Keep compound-identifier retrieval deterministic when broad lexical matches saturate the FTS pool. Release details: - Require an exact identifier anchor before contextual scoring and retain fuzzy and broad backfill. - Cover bounded exact candidates, typo recovery, and distractor-heavy retrieval. Verification: - Run the reproducible S local-hash quality benchmark with 100 of 100 cases passing. - Run pnpm validate.
1 parent ec01027 commit 512119d

2 files changed

Lines changed: 86 additions & 10 deletions

File tree

packages/ragmir-core/src/query.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,53 @@ describe("search", () => {
408408
expect(results[0]?.relativePath).toBe(".ragmir/raw/zeta.md")
409409
})
410410

411+
it("should anchor compound identifiers before broad lexical backfill", async () => {
412+
const root = await mkdtemp(path.join(os.tmpdir(), "ragmir-query-identifier-anchor-"))
413+
tempDirs.push(root)
414+
await initProject(root)
415+
await mkdir(path.join(root, ".ragmir", "raw"), { recursive: true })
416+
await writeFile(
417+
path.join(root, ".ragmir", "config.json"),
418+
JSON.stringify({ retrievalProfile: "fast", topK: 10 }),
419+
)
420+
await Promise.all([
421+
...Array.from({ length: 50 }, (_entry, index) =>
422+
writeFile(
423+
path.join(root, ".ragmir", "raw", `target-${String(index).padStart(3, "0")}.md`),
424+
`Find evidence for compound identifier BENCH-IDENTIFIER-14 target ${index}.\n`,
425+
),
426+
),
427+
...Array.from({ length: 120 }, (_entry, index) =>
428+
writeFile(
429+
path.join(root, ".ragmir", "raw", `distractor-${String(index).padStart(3, "0")}.md`),
430+
`Find broad evidence for an unrelated routine ${index}.\n`,
431+
),
432+
),
433+
])
434+
await ingest({ cwd: root })
435+
436+
const exact = await search("Find evidence for BENCH-IDENTIFIER-14", {
437+
cwd: root,
438+
topK: 10,
439+
explain: true,
440+
})
441+
const fuzzy = await search("Find evidence for BENCH-IDENTIFIXR-14", {
442+
cwd: root,
443+
topK: 1,
444+
explain: true,
445+
})
446+
447+
expect(exact).toHaveLength(10)
448+
expect(exact.every((result) => result.text.includes("BENCH-IDENTIFIER-14"))).toBe(true)
449+
expect(exact[0]?.score).toMatchObject({
450+
lexicalBackend: "fts",
451+
lexicalCandidatesMaterialized: 50,
452+
lexicalQueryVariants: 1,
453+
})
454+
expect(fuzzy[0]?.text).toContain("BENCH-IDENTIFIER-14")
455+
expect(fuzzy[0]?.score?.lexicalQueryVariants).toBeGreaterThan(1)
456+
}, 15_000)
457+
411458
it("should scan a complete lexical fallback in bounded batches", async () => {
412459
const root = await mkdtemp(path.join(os.tmpdir(), "ragmir-query-truncated-fallback-"))
413460
tempDirs.push(root)

packages/ragmir-core/src/query.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { channel } from "node:diagnostics_channel"
22
import {
3+
BooleanQuery,
34
type Connection,
45
type FullTextQuery,
56
MatchQuery,
7+
Occur,
68
Operator,
79
PhraseQuery,
810
} from "@lancedb/lancedb"
@@ -977,20 +979,47 @@ function lexicalQuery(
977979
return null
978980
}
979981
const joined = tokens.join(" ")
982+
const broadQuery = new MatchQuery(joined, "searchText", { operator: Operator.Or })
980983
const supplemental: FullTextQuery[] = []
981-
if (tokens.length > 1) {
982-
supplemental.push(new PhraseQuery(joined, "searchText"))
983-
}
984984
const identifierTerms = [...query.matchAll(LEXICAL_IDENTIFIER_PATTERN)]
985985
.map((match) => match[0])
986986
.filter(Boolean)
987-
for (const identifier of [...new Set(identifierTerms)]) {
988-
supplemental.push(
989-
new MatchQuery(identifier, "searchText", {
990-
boost: 2,
991-
...(isFuzzyLexicalTerm(identifier) ? { fuzziness: 1, prefixLength: 3 } : {}),
992-
}),
987+
const identifiers = [...new Set(identifierTerms)]
988+
const firstIdentifier = identifiers[0]
989+
if (firstIdentifier !== undefined) {
990+
const firstExactIdentifierQuery = new PhraseQuery(firstIdentifier, "searchText")
991+
const exactIdentifierQueries = [
992+
firstExactIdentifierQuery,
993+
...identifiers.slice(1).map((identifier) => new PhraseQuery(identifier, "searchText")),
994+
]
995+
const exactIdentifierQuery: FullTextQuery =
996+
exactIdentifierQueries.length === 1
997+
? firstExactIdentifierQuery
998+
: new BooleanQuery(
999+
exactIdentifierQueries.map((item): [Occur, FullTextQuery] => [Occur.Should, item]),
1000+
)
1001+
const fuzzyIdentifierQuery = new BooleanQuery(
1002+
identifiers.map((identifier): [Occur, FullTextQuery] => [
1003+
Occur.Should,
1004+
new MatchQuery(identifier, "searchText", {
1005+
boost: 2,
1006+
fuzziness: 1,
1007+
operator: Operator.And,
1008+
prefixLength: 3,
1009+
}),
1010+
]),
9931011
)
1012+
supplemental.push(fuzzyIdentifierQuery, broadQuery)
1013+
return {
1014+
primary: new BooleanQuery([
1015+
[Occur.Must, exactIdentifierQuery],
1016+
[Occur.Should, broadQuery],
1017+
]),
1018+
supplemental,
1019+
}
1020+
}
1021+
if (tokens.length > 1) {
1022+
supplemental.push(new PhraseQuery(joined, "searchText"))
9941023
}
9951024
const rareTerms = tokens
9961025
.filter(isFuzzyLexicalTerm)
@@ -1006,7 +1035,7 @@ function lexicalQuery(
10061035
)
10071036
}
10081037
return {
1009-
primary: new MatchQuery(joined, "searchText", { operator: Operator.Or }),
1038+
primary: broadQuery,
10101039
supplemental,
10111040
}
10121041
}

0 commit comments

Comments
 (0)