Skip to content

Commit 4a82e90

Browse files
author
Zhe Yu
committed
feat(cli): Implement QueryResult type.
1 parent 3f30dcb commit 4a82e90

3 files changed

Lines changed: 158 additions & 0 deletions

File tree

src/vectorcode/chunking.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class Chunk:
3131
def __str__(self):
3232
return self.text
3333

34+
def __hash__(self) -> int:
35+
return hash(f"VectorCodeChunk({self.start}:{self.end}@{self.text})")
36+
3437
def export_dict(self):
3538
if self.start is not None:
3639
return {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import heapq
2+
from collections import defaultdict
3+
from dataclasses import dataclass
4+
from typing import Literal, Optional, Sequence, Union
5+
6+
import numpy
7+
8+
from vectorcode.chunking import Chunk
9+
10+
11+
@dataclass
12+
class QueryResult:
13+
"""
14+
The container for one single query result.
15+
16+
args:
17+
- path: path to the file
18+
- content: `vectorcode.chunking.Chunk` object that stores the chunk
19+
- query: query messages used for the search
20+
- scores: similarity scores for the corresponding query.
21+
"""
22+
23+
path: str
24+
chunk: Chunk
25+
query: Sequence[str]
26+
scores: Sequence[float]
27+
28+
@classmethod
29+
def merge(cls, *results: "QueryResult") -> "QueryResult":
30+
"""
31+
Given the results of a single chunk/document from different queries, merge them into a single `QueryResult` object.
32+
"""
33+
for i in range(len(results) - 1):
34+
if (i < len(results) - 1) and not results[i].is_same_doc(results[i + 1]):
35+
raise ValueError(
36+
f"The inputs are not the same chunk: {results[i]}, {results[i + 1]}"
37+
)
38+
39+
return QueryResult(
40+
path=results[0].path,
41+
chunk=results[0].chunk,
42+
query=sum((tuple(i.query) for i in results), start=tuple()),
43+
scores=sum((tuple(i.scores) for i in results), start=tuple()),
44+
)
45+
46+
@staticmethod
47+
def group(
48+
*results: "QueryResult",
49+
key: Union[Literal["path"], Literal["chunk"]] = "path",
50+
top_k: Optional[int] = None,
51+
) -> dict[Chunk | str, list["QueryResult"]]:
52+
assert key in {"path", "chunk"}
53+
grouped_result: dict[Chunk | str, list["QueryResult"]] = defaultdict(list)
54+
55+
for res in results:
56+
grouped_result[getattr(res, key)].append(res)
57+
58+
if top_k and top_k > 0:
59+
for group in grouped_result.keys():
60+
grouped_result[group] = heapq.nlargest(top_k, grouped_result[group])
61+
return grouped_result
62+
63+
def mean_score(self):
64+
return float(numpy.mean(self.scores))
65+
66+
def __lt__(self, other: "QueryResult"):
67+
return self.mean_score() < other.mean_score()
68+
69+
def __gt__(self, other: "QueryResult"):
70+
return self.mean_score() > other.mean_score()
71+
72+
def is_same_doc(self, other: "QueryResult") -> bool:
73+
return self.path == other.path and self.chunk == other.chunk
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pytest
2+
from tree_sitter import Point
3+
4+
from vectorcode.chunking import Chunk
5+
from vectorcode.subcommands.query.types import QueryResult
6+
7+
8+
def make_dummy_chunk():
9+
return QueryResult(
10+
path="dummy1.py",
11+
chunk=Chunk(
12+
text="hello", start=Point(row=1, column=0), end=Point(row=1, column=4)
13+
),
14+
query=["hello"],
15+
scores=[0.9],
16+
)
17+
18+
19+
def test_QueryResult_merge():
20+
res1, res2 = (make_dummy_chunk(), make_dummy_chunk())
21+
res2.query = ["bye"]
22+
res2.scores = [0.1]
23+
24+
merged = QueryResult.merge(res1, res2)
25+
assert merged.path == res1.path
26+
assert merged.chunk == res1.chunk
27+
assert merged.mean_score() == 0.5
28+
assert merged.query == ("hello", "bye")
29+
30+
31+
def test_QueryResult_merge_failed():
32+
res1, res2 = (make_dummy_chunk(), make_dummy_chunk())
33+
res2.path = "dummy2.py"
34+
with pytest.raises(ValueError):
35+
QueryResult.merge(res1, res2)
36+
37+
38+
def test_QueryResult_group_by_path():
39+
res1, res2 = (make_dummy_chunk(), make_dummy_chunk())
40+
res2.chunk = Chunk(
41+
"hello", start=Point(row=2, column=0), end=Point(row=2, column=4)
42+
)
43+
res2.query = ["bye"]
44+
res2.scores = [0.1]
45+
46+
grouped_dict = QueryResult.group(res1, res2)
47+
assert len(grouped_dict.keys()) == 1
48+
assert len(grouped_dict["dummy1.py"]) == 2
49+
50+
51+
def test_QueryResult_group_by_chunk():
52+
res1, res2 = (make_dummy_chunk(), make_dummy_chunk())
53+
res2.query = ["bye"]
54+
res2.scores = [0.1]
55+
56+
grouped_dict = QueryResult.group(res1, res2, key="chunk")
57+
assert len(grouped_dict.keys()) == 1
58+
assert len(grouped_dict[res1.chunk]) == 2
59+
60+
61+
def test_QueryResult_group_top_k():
62+
res1, res2 = (make_dummy_chunk(), make_dummy_chunk())
63+
res2.chunk = Chunk(
64+
"hello", start=Point(row=2, column=0), end=Point(row=2, column=4)
65+
)
66+
res2.query = ["bye"]
67+
res2.scores = [0.1]
68+
69+
grouped_dict = QueryResult.group(res1, res2, top_k=1)
70+
assert len(grouped_dict.keys()) == 1
71+
assert len(grouped_dict["dummy1.py"]) == 1
72+
assert grouped_dict["dummy1.py"][0].query[0] == "hello"
73+
74+
75+
def test_QueryResult_lt():
76+
res1, res2 = (make_dummy_chunk(), make_dummy_chunk())
77+
res2.chunk = Chunk(
78+
"hello", start=Point(row=2, column=0), end=Point(row=2, column=4)
79+
)
80+
res2.query = ["bye"]
81+
res2.scores = [0.1]
82+
assert res2 < res1

0 commit comments

Comments
 (0)