1+ import re
12import sqlite3
23from pathlib import Path
34
5+ from sqlite_rag .models .document_result import DocumentResult
6+
47from .chunker import Chunker
58from .models .chunk import Chunk
69from .models .document import Document
@@ -46,13 +49,13 @@ def generate_embedding(self, chunks: list[Chunk]) -> list[Chunk]:
4649 cursor = self ._conn .cursor ()
4750
4851 for chunk in chunks :
49- cursor .execute ("SELECT llm_embed_generate(?)" , (chunk .content ,))
52+ cursor .execute ("SELECT llm_embed_generate(?) AS embedding " , (chunk .content ,))
5053 result = cursor .fetchone ()
5154
5255 if result is None :
5356 raise RuntimeError ("Failed to generate embedding." )
5457
55- chunk .embedding = result [0 ]
58+ chunk .embedding = result ['embedding' ]
5659
5760 return chunks
5861
@@ -64,42 +67,40 @@ def quantize(self) -> None:
6467
6568 self ._conn .commit ()
6669
67- def search (self , query : str , limit : int = 10 ) -> list [Document ]:
70+ def search (self , query : str , limit : int = 10 ) -> list [DocumentResult ]:
6871 """Semantic search and full-text search sorted with Reciprocal Rank Fusion."""
6972 cursor = self ._conn .cursor ()
7073
7174 query_embedding = self .generate_embedding ([Chunk (content = query )])[0 ].embedding
7275
76+ # Clean up and split into words
77+ query = " " .join (re .findall (r"\b\w+\b" , query .lower ()))
78+
7379 cursor .execute (
7480 # TODO: use vector_convert_XXX to convert the query to the correct type
7581 """
7682 -- sqlite-vector KNN vector search results
7783 WITH vec_matches AS (
7884 SELECT
85+ v.rowid AS chunk_id,
7986 row_number() over (order by v.distance) AS rank_number,
80- c.document_id,
8187 v.distance
82- FROM chunks AS c
83- JOIN vector_quantize_scan('chunks', 'embedding', vector_convert_f32(:query_embedding), :k) AS v
84- ON c.rowid = v.rowid
88+ FROM vector_quantize_scan('chunks', 'embedding', vector_convert_f32(:query_embedding), :k) AS v
8589 ),
8690 -- Full-text search results
8791 fts_matches AS (
8892 SELECT
93+ chunks_fts.rowid AS chunk_id,
8994 row_number() over (order by rank) AS rank_number,
90- c.document_id,
9195 rank AS score
92- FROM chunks_fts AS c_fts
93- JOIN chunks AS c ON c_fts.rowid = c.rowid
94- WHERE c_fts.content MATCH :query
96+ FROM chunks_fts
97+ WHERE chunks_fts MATCH :query
9598 LIMIT :k
9699 ),
97100 -- combine FTS5 + vector search results with RRF
98101 matches AS (
99102 SELECT
100- documents.id,
101- documents.uri,
102- documents.content,
103+ COALESCE(vec_matches.chunk_id, fts_matches.chunk_id) AS chunk_id,
103104 vec_matches.rank_number AS vec_rank,
104105 fts_matches.rank_number AS fts_rank,
105106 -- Reciprocal Rank Fusion score
@@ -111,22 +112,26 @@ def search(self, query: str, limit: int = 10) -> list[Document]:
111112 fts_matches.score AS fts_score
112113 FROM vec_matches
113114 FULL OUTER JOIN fts_matches
114- ON vec_matches.document_id = fts_matches.document_id
115- JOIN documents ON documents.id = COALESCE(vec_matches.document_id, fts_matches.document_id)
116- ORDER BY combined_rank DESC
115+ ON vec_matches.chunk_id = fts_matches.chunk_id
117116 )
118117 SELECT
119- id,
120- uri,
121- content,
118+ documents.id,
119+ documents.uri,
120+ documents.content as document_content,
121+ chunks.content AS snippet,
122122 vec_rank,
123123 fts_rank,
124124 combined_rank,
125125 vec_distance,
126126 fts_score
127- FROM matches;
127+ FROM matches
128+ JOIN chunks ON chunks.id = matches.chunk_id
129+ JOIN documents ON documents.id = chunks.document_id
130+ ORDER BY combined_rank DESC
131+ ;
128132 """ ,
129133 {
134+ # '*' is used to match while typing
130135 "query" : query + "*" ,
131136 "query_embedding" : query_embedding ,
132137 "k" : limit ,
@@ -139,15 +144,18 @@ def search(self, query: str, limit: int = 10) -> list[Document]:
139144
140145 rows = cursor .fetchall ()
141146 return [
142- Document (
143- id = row [0 ],
144- uri = row [1 ],
145- content = row [2 ],
146- vec_rank = row [3 ],
147- fts_rank = row [4 ],
148- combined_rank = row [5 ],
149- vec_distance = row [6 ],
150- fts_score = row [7 ],
147+ DocumentResult (
148+ document = Document (
149+ id = row ["id" ],
150+ uri = row ["uri" ],
151+ content = row ["document_content" ],
152+ ),
153+ snippet = row ["snippet" ],
154+ vec_rank = row ["vec_rank" ],
155+ fts_rank = row ["fts_rank" ],
156+ combined_rank = row ["combined_rank" ],
157+ vec_distance = row ["vec_distance" ],
158+ fts_score = row ["fts_score" ],
151159 )
152160 for row in rows
153161 ]
0 commit comments