Skip to content

Commit 12b94e7

Browse files
committed
fix: handle current directory path "." correctly in codebase_search tool
- Fix path filtering logic in QdrantVectorStore.search() to properly handle current directory representations - When directoryPrefix is ".", "./", "", or similar, set filter to undefined to search entire workspace - Add comprehensive tests covering various current directory path formats including cross-platform support - Resolves issue where codebase_search with path="." returned no results Fixes #6514
1 parent 5041880 commit 12b94e7

File tree

2 files changed

+219
-7
lines changed

2 files changed

+219
-7
lines changed

src/services/code-index/vector-store/__tests__/qdrant-client.spec.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,5 +1526,209 @@ describe("QdrantVectorStore", () => {
15261526
expect(callArgs.limit).toBe(DEFAULT_MAX_SEARCH_RESULTS)
15271527
expect(callArgs.score_threshold).toBe(DEFAULT_SEARCH_MIN_SCORE)
15281528
})
1529+
1530+
describe("current directory path handling", () => {
1531+
it("should not apply filter when directoryPrefix is '.'", async () => {
1532+
const queryVector = [0.1, 0.2, 0.3]
1533+
const directoryPrefix = "."
1534+
const mockQdrantResults = {
1535+
points: [
1536+
{
1537+
id: "test-id-1",
1538+
score: 0.85,
1539+
payload: {
1540+
filePath: "src/test.ts",
1541+
codeChunk: "test code",
1542+
startLine: 1,
1543+
endLine: 5,
1544+
pathSegments: { "0": "src", "1": "test.ts" },
1545+
},
1546+
},
1547+
],
1548+
}
1549+
1550+
mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
1551+
1552+
const results = await vectorStore.search(queryVector, directoryPrefix)
1553+
1554+
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
1555+
query: queryVector,
1556+
filter: undefined, // Should be undefined for current directory
1557+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1558+
limit: DEFAULT_MAX_SEARCH_RESULTS,
1559+
params: {
1560+
hnsw_ef: 128,
1561+
exact: false,
1562+
},
1563+
with_payload: {
1564+
include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
1565+
},
1566+
})
1567+
1568+
expect(results).toEqual(mockQdrantResults.points)
1569+
})
1570+
1571+
it("should not apply filter when directoryPrefix is './'", async () => {
1572+
const queryVector = [0.1, 0.2, 0.3]
1573+
const directoryPrefix = "./"
1574+
const mockQdrantResults = { points: [] }
1575+
1576+
mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
1577+
1578+
await vectorStore.search(queryVector, directoryPrefix)
1579+
1580+
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
1581+
query: queryVector,
1582+
filter: undefined, // Should be undefined for current directory
1583+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1584+
limit: DEFAULT_MAX_SEARCH_RESULTS,
1585+
params: {
1586+
hnsw_ef: 128,
1587+
exact: false,
1588+
},
1589+
with_payload: {
1590+
include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
1591+
},
1592+
})
1593+
})
1594+
1595+
it("should not apply filter when directoryPrefix is empty string", async () => {
1596+
const queryVector = [0.1, 0.2, 0.3]
1597+
const directoryPrefix = ""
1598+
const mockQdrantResults = { points: [] }
1599+
1600+
mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
1601+
1602+
await vectorStore.search(queryVector, directoryPrefix)
1603+
1604+
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
1605+
query: queryVector,
1606+
filter: undefined, // Should be undefined for empty string
1607+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1608+
limit: DEFAULT_MAX_SEARCH_RESULTS,
1609+
params: {
1610+
hnsw_ef: 128,
1611+
exact: false,
1612+
},
1613+
with_payload: {
1614+
include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
1615+
},
1616+
})
1617+
})
1618+
1619+
it("should not apply filter when directoryPrefix is '.\\' (Windows style)", async () => {
1620+
const queryVector = [0.1, 0.2, 0.3]
1621+
const directoryPrefix = ".\\"
1622+
const mockQdrantResults = { points: [] }
1623+
1624+
mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
1625+
1626+
await vectorStore.search(queryVector, directoryPrefix)
1627+
1628+
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
1629+
query: queryVector,
1630+
filter: undefined, // Should be undefined for Windows current directory
1631+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1632+
limit: DEFAULT_MAX_SEARCH_RESULTS,
1633+
params: {
1634+
hnsw_ef: 128,
1635+
exact: false,
1636+
},
1637+
with_payload: {
1638+
include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
1639+
},
1640+
})
1641+
})
1642+
1643+
it("should not apply filter when directoryPrefix has trailing slashes", async () => {
1644+
const queryVector = [0.1, 0.2, 0.3]
1645+
const directoryPrefix = ".///"
1646+
const mockQdrantResults = { points: [] }
1647+
1648+
mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
1649+
1650+
await vectorStore.search(queryVector, directoryPrefix)
1651+
1652+
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
1653+
query: queryVector,
1654+
filter: undefined, // Should be undefined after normalizing trailing slashes
1655+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1656+
limit: DEFAULT_MAX_SEARCH_RESULTS,
1657+
params: {
1658+
hnsw_ef: 128,
1659+
exact: false,
1660+
},
1661+
with_payload: {
1662+
include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
1663+
},
1664+
})
1665+
})
1666+
1667+
it("should still apply filter for relative paths like './src'", async () => {
1668+
const queryVector = [0.1, 0.2, 0.3]
1669+
const directoryPrefix = "./src"
1670+
const mockQdrantResults = { points: [] }
1671+
1672+
mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
1673+
1674+
await vectorStore.search(queryVector, directoryPrefix)
1675+
1676+
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
1677+
query: queryVector,
1678+
filter: {
1679+
must: [
1680+
{
1681+
key: "pathSegments.0",
1682+
match: { value: "." },
1683+
},
1684+
{
1685+
key: "pathSegments.1",
1686+
match: { value: "src" },
1687+
},
1688+
],
1689+
}, // Should still create filter for actual relative paths
1690+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1691+
limit: DEFAULT_MAX_SEARCH_RESULTS,
1692+
params: {
1693+
hnsw_ef: 128,
1694+
exact: false,
1695+
},
1696+
with_payload: {
1697+
include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
1698+
},
1699+
})
1700+
})
1701+
1702+
it("should still apply filter for regular directory paths", async () => {
1703+
const queryVector = [0.1, 0.2, 0.3]
1704+
const directoryPrefix = "src"
1705+
const mockQdrantResults = { points: [] }
1706+
1707+
mockQdrantClientInstance.query.mockResolvedValue(mockQdrantResults)
1708+
1709+
await vectorStore.search(queryVector, directoryPrefix)
1710+
1711+
expect(mockQdrantClientInstance.query).toHaveBeenCalledWith(expectedCollectionName, {
1712+
query: queryVector,
1713+
filter: {
1714+
must: [
1715+
{
1716+
key: "pathSegments.0",
1717+
match: { value: "src" },
1718+
},
1719+
],
1720+
}, // Should still create filter for regular paths
1721+
score_threshold: DEFAULT_SEARCH_MIN_SCORE,
1722+
limit: DEFAULT_MAX_SEARCH_RESULTS,
1723+
params: {
1724+
hnsw_ef: 128,
1725+
exact: false,
1726+
},
1727+
with_payload: {
1728+
include: ["filePath", "codeChunk", "startLine", "endLine", "pathSegments"],
1729+
},
1730+
})
1731+
})
1732+
})
15291733
})
15301734
})

src/services/code-index/vector-store/qdrant-client.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,21 @@ export class QdrantVectorStore implements IVectorStore {
375375
let filter = undefined
376376

377377
if (directoryPrefix) {
378-
const segments = directoryPrefix.split(path.sep).filter(Boolean)
379-
380-
filter = {
381-
must: segments.map((segment, index) => ({
382-
key: `pathSegments.${index}`,
383-
match: { value: segment },
384-
})),
378+
// Check if the path represents current directory
379+
const normalizedPrefix = directoryPrefix.replace(/\\/g, "/").replace(/\/+$/, "")
380+
if (normalizedPrefix === "." || normalizedPrefix === "" || normalizedPrefix === "./") {
381+
// Don't create a filter - search entire workspace
382+
filter = undefined
383+
} else {
384+
const segments = directoryPrefix.split(path.sep).filter(Boolean)
385+
if (segments.length > 0) {
386+
filter = {
387+
must: segments.map((segment, index) => ({
388+
key: `pathSegments.${index}`,
389+
match: { value: segment },
390+
})),
391+
}
392+
}
385393
}
386394
}
387395

0 commit comments

Comments
 (0)