Skip to content

Commit a5f1bb9

Browse files
author
Zhe Yu
committed
refactor(cli): Use in-house QueryResult in the query subcommand and the rerankers.
1 parent 4a82e90 commit a5f1bb9

6 files changed

Lines changed: 102 additions & 86 deletions

File tree

src/vectorcode/chunking.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ class Chunk:
2525
"""
2626

2727
text: str
28-
start: Point
29-
end: Point
28+
start: Point | None = None
29+
end: Point | None = None
3030

3131
def __str__(self):
3232
return self.text
@@ -35,14 +35,20 @@ def __hash__(self) -> int:
3535
return hash(f"VectorCodeChunk({self.start}:{self.end}@{self.text})")
3636

3737
def export_dict(self):
38+
d: dict[str, str | dict[str, int]] = {"text": self.text}
3839
if self.start is not None:
39-
return {
40-
"text": self.text,
41-
"start": {"row": self.start.row, "column": self.start.column},
42-
"end": {"row": self.end.row, "column": self.end.column},
43-
}
44-
else:
45-
return {"text": self.text}
40+
d.update(
41+
{
42+
"start": {"row": self.start.row, "column": self.start.column},
43+
}
44+
)
45+
if self.end is not None:
46+
d.update(
47+
{
48+
"end": {"row": self.end.row, "column": self.end.column},
49+
}
50+
)
51+
return d
4652

4753

4854
@dataclass

src/vectorcode/subcommands/query/__init__.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
from chromadb import GetResult, Where
77
from chromadb.api.models.AsyncCollection import AsyncCollection
8-
from chromadb.api.types import IncludeEnum
8+
from chromadb.api.types import IncludeEnum, QueryResult
99
from chromadb.errors import InvalidCollectionException, InvalidDimensionException
10+
from tree_sitter import Point
1011

11-
from vectorcode.chunking import StringChunker
12+
from vectorcode.chunking import Chunk, StringChunker
1213
from vectorcode.cli_utils import (
1314
Config,
1415
QueryInclude,
@@ -22,6 +23,7 @@
2223
get_embedding_function,
2324
verify_ef,
2425
)
26+
from vectorcode.subcommands.query import types as vectorcode_types
2527
from vectorcode.subcommands.query.reranker import (
2628
RerankerError,
2729
get_reranker,
@@ -30,14 +32,45 @@
3032
logger = logging.getLogger(name=__name__)
3133

3234

35+
def conver_query_results(
36+
chroma_result: QueryResult, queries: list[str]
37+
) -> list[vectorcode_types.QueryResult]:
38+
"""Convert chromadb query result to in-house query results"""
39+
assert chroma_result["documents"] is not None
40+
assert chroma_result["distances"] is not None
41+
assert chroma_result["metadatas"] is not None
42+
43+
chroma_results_list: list[vectorcode_types.QueryResult] = []
44+
for q_i in range(len(queries)):
45+
q = queries[q_i]
46+
documents = chroma_result["documents"][q_i]
47+
distances = chroma_result["distances"][q_i]
48+
metadatas = chroma_result["metadatas"][q_i]
49+
for doc, dist, meta in zip(documents, distances, metadatas):
50+
chunk = Chunk(text=doc)
51+
if meta["start"]:
52+
chunk.start = Point(int(meta.get("start", 0)), 0)
53+
if meta["end"]:
54+
chunk.end = Point(int(meta.get("end", 0)) + 1, 0)
55+
chroma_results_list.append(
56+
vectorcode_types.QueryResult(
57+
chunk=chunk,
58+
path=str(meta.get("path", "")),
59+
query=(q,),
60+
scores=(-dist,),
61+
)
62+
)
63+
return chroma_results_list
64+
65+
3366
async def get_query_result_files(
3467
collection: AsyncCollection, configs: Config
3568
) -> list[str]:
3669
query_chunks = []
37-
if configs.query:
38-
chunker = StringChunker(configs)
39-
for q in configs.query:
40-
query_chunks.extend(str(i) for i in chunker.chunk(q))
70+
assert configs.query, "Query messages cannot be empty."
71+
chunker = StringChunker(configs)
72+
for q in configs.query:
73+
query_chunks.extend(str(i) for i in chunker.chunk(q))
4174

4275
configs.query_exclude = [
4376
expand_path(i, True)
@@ -70,7 +103,7 @@ async def get_query_result_files(
70103
query_embeddings = get_embedding_function(configs)(query_chunks)
71104
if isinstance(configs.embedding_dims, int) and configs.embedding_dims > 0:
72105
query_embeddings = [e[: configs.embedding_dims] for e in query_embeddings]
73-
results = await collection.query(
106+
chroma_query_results: QueryResult = await collection.query(
74107
query_embeddings=query_embeddings,
75108
n_results=num_query,
76109
include=[
@@ -85,7 +118,9 @@ async def get_query_result_files(
85118
return []
86119

87120
reranker = get_reranker(configs)
88-
return await reranker.rerank(results)
121+
return await reranker.rerank(
122+
conver_query_results(chroma_query_results, configs.query)
123+
)
89124

90125

91126
async def build_query_results(
Lines changed: 24 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import heapq
22
import logging
33
from abc import ABC, abstractmethod
4-
from collections import defaultdict
5-
from typing import Any, DefaultDict, Optional, Sequence, cast
4+
from typing import Any
65

76
import numpy
8-
from chromadb.api.types import QueryResult
97

8+
from vectorcode.chunking import Chunk
109
from vectorcode.cli_utils import Config, QueryInclude
10+
from vectorcode.subcommands.query.types import QueryResult
1111

1212
logger = logging.getLogger(name=__name__)
1313

@@ -29,7 +29,7 @@ def __init__(self, configs: Config, **kwargs: Any):
2929
"'configs' should contain the query messages."
3030
)
3131
self.n_result = configs.n_result
32-
self._raw_results: Optional[QueryResult] = None
32+
self._raw_results: list[QueryResult] = []
3333

3434
@classmethod
3535
def create(cls, configs: Config, **kwargs: Any):
@@ -46,53 +46,31 @@ def create(cls, configs: Config, **kwargs: Any):
4646
raise
4747

4848
@abstractmethod
49-
async def compute_similarity(
50-
self, results: list[str], query_message: str
51-
) -> Sequence[float]: # pragma: nocover
52-
"""Given a list of n results and 1 query message,
53-
return a list-like object of length n that contains the similarity scores between
54-
each item in `results` and the `query_message`.
55-
56-
A high similarity score means the strings are semantically similar to each other.
57-
`query_message` will be loaded in the same order as they appear in `self.configs.query`.
58-
59-
If you need the raw query results from chromadb,
60-
it'll be saved in `self._raw_results` before this method is called.
49+
async def compute_similarity(self, results: list[QueryResult]): # pragma: nocover
50+
"""
51+
Modify the `QueryResult.scores` field IN-PLACE so that they contain the correct scores.
6152
"""
6253
raise NotImplementedError
6354

64-
async def rerank(self, results: QueryResult | dict) -> list[str]:
65-
if len(results["ids"]) == 0 or all(len(i) == 0 for i in results["ids"]):
55+
async def rerank(self, results: list[QueryResult]) -> list[str]:
56+
if len(results) == 0:
6657
return []
58+
results = await self.compute_similarity(results)
6759

68-
self._raw_results = cast(QueryResult, results)
69-
query_chunks = self.configs.query
70-
assert query_chunks
71-
assert results["metadatas"] is not None
72-
assert results["documents"] is not None
73-
documents: DefaultDict[str, list[float]] = defaultdict(list)
74-
for query_chunk_idx in range(len(query_chunks)):
75-
chunk_ids = results["ids"][query_chunk_idx]
76-
chunk_metas = results["metadatas"][query_chunk_idx]
77-
chunk_docs = results["documents"][query_chunk_idx]
78-
scores = await self.compute_similarity(
79-
chunk_docs, query_chunks[query_chunk_idx]
80-
)
81-
for i, score in enumerate(scores):
82-
if QueryInclude.chunk in self.configs.include:
83-
documents[chunk_ids[i]].append(float(score))
84-
else:
85-
documents[str(chunk_metas[i]["path"])].append(float(score))
60+
group_by = "path"
61+
if QueryInclude.chunk in self.configs.include:
62+
group_by = "chunk"
63+
grouped_results = QueryResult.group(*results, key=group_by, top_k="auto")
8664

87-
logger.debug("Document scores: %s", documents)
88-
top_k = int(numpy.mean(tuple(len(i) for i in documents.values())))
89-
for key in documents.keys():
90-
documents[key] = heapq.nlargest(top_k, documents[key])
91-
92-
self._raw_results = None
65+
scores: dict[Chunk | str, float] = {}
66+
for key in grouped_results.keys():
67+
scores[key] = float(
68+
numpy.mean(tuple(i.mean_score() for i in grouped_results[key]))
69+
)
9370

94-
return heapq.nlargest(
95-
self.n_result,
96-
documents.keys(),
97-
key=lambda x: float(numpy.mean(documents[x])),
71+
return list(
72+
str(i)
73+
for i in heapq.nlargest(
74+
self.configs.n_result, grouped_results.keys(), key=lambda x: scores[x]
75+
)
9876
)

src/vectorcode/subcommands/query/reranker/cross_encoder.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import asyncio
21
import logging
32
from typing import Any
43

54
from vectorcode.cli_utils import Config
5+
from vectorcode.subcommands.query.types import QueryResult
66

77
from .base import RerankerBase
88

@@ -34,8 +34,8 @@ def __init__(
3434
model_name = configs.reranker_params.pop("model_name_or_path")
3535
self.model = CrossEncoder(model_name, **configs.reranker_params)
3636

37-
async def compute_similarity(self, results: list[str], query_message: str):
38-
scores = await asyncio.to_thread(
39-
self.model.predict, [(chunk, query_message) for chunk in results]
40-
)
41-
return list(float(i) for i in scores)
37+
async def compute_similarity(self, results: list[QueryResult]):
38+
scores = self.model.predict([(str(res.chunk), res.query[0]) for res in results])
39+
40+
for res, score in zip(results, scores):
41+
res.scores = (score,)
Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import logging
2-
from typing import Any, Sequence
2+
from typing import Any
33

44
from vectorcode.cli_utils import Config
5+
from vectorcode.subcommands.query.types import QueryResult
56

67
from .base import RerankerBase
78

@@ -17,15 +18,8 @@ class NaiveReranker(RerankerBase):
1718
def __init__(self, configs: Config, **kwargs: Any):
1819
super().__init__(configs)
1920

20-
async def compute_similarity(
21-
self, results: list[str], query_message: str
22-
) -> Sequence[float]:
23-
assert self._raw_results is not None, "Expecting raw results from the database."
24-
assert self._raw_results.get("distances") is not None
25-
assert self.configs.query, "Expecting query messages in self.configs"
26-
idx = self.configs.query.index(query_message)
27-
dist = self._raw_results.get("distances")
28-
if dist is None: # pragma: nocover
29-
raise ValueError("QueryResult should contain distances!")
30-
else:
31-
return list(-i for i in dist[idx])
21+
async def compute_similarity(self, results: list[QueryResult]):
22+
"""
23+
Do nothing, because the QueryResult objects already contain distances.
24+
"""
25+
pass

src/vectorcode/subcommands/query/types.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import heapq
22
from collections import defaultdict
33
from dataclasses import dataclass
4-
from typing import Literal, Optional, Sequence, Union
4+
from typing import Literal, Union
55

66
import numpy
77

@@ -22,8 +22,8 @@ class QueryResult:
2222

2323
path: str
2424
chunk: Chunk
25-
query: Sequence[str]
26-
scores: Sequence[float]
25+
query: tuple[str, ...]
26+
scores: tuple[float, ...]
2727

2828
@classmethod
2929
def merge(cls, *results: "QueryResult") -> "QueryResult":
@@ -47,14 +47,17 @@ def merge(cls, *results: "QueryResult") -> "QueryResult":
4747
def group(
4848
*results: "QueryResult",
4949
key: Union[Literal["path"], Literal["chunk"]] = "path",
50-
top_k: Optional[int] = None,
50+
top_k: int | Literal["auto"] | None = None,
5151
) -> dict[Chunk | str, list["QueryResult"]]:
5252
assert key in {"path", "chunk"}
5353
grouped_result: dict[Chunk | str, list["QueryResult"]] = defaultdict(list)
5454

5555
for res in results:
5656
grouped_result[getattr(res, key)].append(res)
5757

58+
if top_k == "auto":
59+
top_k = int(numpy.mean(tuple(len(i) for i in grouped_result.values())))
60+
5861
if top_k and top_k > 0:
5962
for group in grouped_result.keys():
6063
grouped_result[group] = heapq.nlargest(top_k, grouped_result[group])

0 commit comments

Comments
 (0)